简体   繁体   English

将阻塞调用包装为异步以获得更好的线程重用和响应式UI

[英]Wrapping blocking calls to be async for better thread reuse and responsive UI

I have a class that is responsible for retrieving a product availability by making call to a legacy class. 我有一个类负责通过调用遗留类来检索产品可用性。 This legacy class itself internally collects product data by making BLOCKING network calls. 此遗留类本身通过进行BLOCKING网络调用来内部收集产品数据。 Note that I cannot modify code of legacy API. 请注意,我无法修改旧版API的代码。 Since all products are independent to each other, I would like to parallelise collecting the information without creating any unnecessary threads and also not blocking thread that gets blocked on calling this legacy API. 由于所有产品彼此独立,因此我希望并行化收集信息,而不会创建任何不必要的线程,也不会阻塞在调用此旧版API时被阻塞的线程。 With this background here are my basic classes. 有了这个背景,这里是我的基本课程。

class Product
    {
        public int ID { get; set; }
        public int  VendorID { get; set; }
        public string Name { get; set; }
    }

    class ProductSearchResult
    {
        public int ID { get; set; }
        public int AvailableQuantity { get; set; }
        public DateTime ShipDate { get; set; }
        public bool Success { get; set; }
        public string Error { get; set; }
    }

class ProductProcessor
    {
        List<Product> products;
        private static readonly SemaphoreSlim mutex = new SemaphoreSlim(2);
        CancellationTokenSource cts = new CancellationTokenSource();
        public ProductProcessor()
        {
            products = new List<Product>()
            {
                new Product() { ID = 1, VendorID = 100, Name = "PC" },
                new Product() { ID = 2, VendorID = 101, Name = "Tablet" },
                new Product() { ID = 3, VendorID = 100, Name = "Laptop" },
                new Product() { ID = 4, VendorID = 102, Name = "GPS" },
                new Product() { ID = 5, VendorID = 107, Name = "Mars Rover" }
            };

        }

        public async void Start()
        {
            Task<ProductSearchResult>[] tasks = new Task<ProductSearchResult>[products.Count];
            Parallel.For(0, products.Count(), async i =>
            {
                tasks[i] = RetrieveProductAvailablity(products[i].ID, cts.Token);

            });



            Task<ProductSearchResult> results = await Task.WhenAny(tasks);

            // Logic for waiting on indiviaul tasks and reporting results

        }

        private async Task<ProductSearchResult> RetrieveProductAvailablity(int productId, CancellationToken cancellationToken)
        {
            ProductSearchResult result = new ProductSearchResult();
            result.ID = productId;

            if (cancellationToken.IsCancellationRequested)
            {
                result.Success = false;
                result.Error = "Cancelled.";
                return result;
            }

            try
            {
                await mutex.WaitAsync();
                if (cancellationToken.IsCancellationRequested)
                {
                    result.Success = false;
                    result.Error = "Cancelled.";
                    return result;
                }

                LegacyApp app = new LegacyApp();
                bool success = await Task.Run(() => app.RetrieveProductAvailability(productId));
                if (success)
                {
                    result.Success = success;
                    result.AvailableQuantity = app.AvailableQuantity;
                    result.ShipDate = app.ShipDate;
                }
                else
                {
                    result.Success = false;
                    result.Error = app.Error;
                }
            }
            finally
            {
                mutex.Release();
            }

            return result;

        }

    }

Given that I am trying to wrap async over a synchronous API, I have two questions. 鉴于我试图通过同步API包装异步,我有两个问题。

  1. With use of Parallel.For and wrapping Legay API call within a Task.Run, am I creating any unnecessary threads that could have been avoided without blocking calling thread as we will use this code in UI. 使用Parallel.For并在Task.Run中包装Legay API调用,我创建了任何不必要的线程,可以避免阻塞调用线程,因为我们将在UI中使用此代码。
  2. Is this code still look thread safe. 这段代码是否仍然看起来是线程安全的。

The compiler will give you warnings about your async lambda. 编译器会给你关于async lambda的警告。 Read it carefully; 仔细阅读; it's telling you that it's not asynchronous. 它告诉你它不是异步的。 There's no point in using async there. 在那里使用async是没有意义的。 Also, do not use async void . 另外,不要使用async void

Since your underlying API is blocking - and there's no way to change that - asynchronous code isn't an option. 由于您的基础API处于阻塞状态,并且无法更改,因此异步代码不是一种选择。 I'd recommend either using several Task.Run calls or Parallel.For , but not both. 我建议使用几个Task.Run调用 Parallel.For ,但不能同时使用。 So let's use parallel. 所以让我们使用并行。 Actually, let's use Parallel LINQ since you're transforming a sequence. 实际上,由于您要转换序列,因此我们使用并行LINQ。

There's no point in making RetrieveProductAvailablity asynchronous; 使RetrieveProductAvailablity异步是没有意义的; it's only doing blocking work except for the throttling, and the parallel approach has more natural throttling support. 除了限制之外,它只进行阻塞工作,并行方法具有更自然的限制支持。 This leaves your method looking like: 这使您的方法看起来像:

private ProductSearchResult RetrieveProductAvailablity(int productId, CancellationToken cancellationToken)
{
  ... // no mutex code
  LegacyApp app = new LegacyApp();
  bool success = app.RetrieveProductAvailability(productId);
  ... // no mutex code
}

You can then do parallel processing as such: 然后,您可以执行以下并行处理:

public void Start()
{
  ProductSearchResult[] results = products.AsParallel().AsOrdered()
      .WithCancellation(cts.Token).WithDegreeOfParallelism(2)
      .Select(product => RetrieveProductAvailability(product.ID, cts.Token))
      .ToArray();
  // Logic for waiting on indiviaul tasks and reporting results
}

From your UI thread, you can call the method using Task.Run : 在UI线程中,可以使用Task.Run 调用方法:

async void MyUiEventHandler(...)
{
  await Task.Run(() => processor.Start());
}

This keeps your business logic clean (only synchronous/parallel code), and the responsibility for moving this work off the UI thread (using Task.Run ) belongs to the UI layer. 这使您的业务逻辑保持干净(仅限同步/并行代码),并且将此工作从UI线程移动(使用Task.Run )的责任属于UI层。

Update: I added a call to AsOrdered to ensure the results array has the same order as the products sequence. 更新:我添加了对AsOrdered的调用,以确保结果数组与产品序列具有相同的顺序。 This may or may not be necessary, but since the original code preserved order, this code does now too. 这可能是必需的,也可能不是必需的,但是由于原始代码保留了顺序,因此现在也是如此。

Update: Since you need to update the UI after every retrieval, you should probably use Task.Run for each one instead of AsParallel : 更新:由于每次检索后都需要更新UI,因此您可能应该对每个UI使用Task.Run而不是AsParallel

public async Task Start()
{
  var tasks = products.Select(product =>
      ProcessAvailabilityAsync(product.ID, cts.Token));
  await Task.WhenAll(tasks);
}

private SemaphoreSlim mutex = new SempahoreSlim(2);
private async Task ProcessAvailabilityAsync(int id, CancellationToken token)
{
  await mutex.WaitAsync();
  try
  {
    var result = await RetrieveProductAvailability(id, token);
    // Logic for reporting results
  }
  finally
  {
    mutex.Release();
  }
}

am I creating any unnecessary threads that could have been avoided without blocking calling thread as we will use this code in UI. 我是否创建了任何不必要的线程,可以避免阻塞调用线程,因为我们将在UI中使用此代码。

Yes. 是。 Your code spins new threads via Parallel.ForEach , and then again internally inside RetrieveProductAvailablity . 您的代码通过Parallel.ForEach旋转新线程,然后在RetrieveProductAvailablity内部再次旋转。 There is no need for that. 没有必要这样做。

async-await and Parallel.ForEach don't really play nice together , as it converts your async lambda into an async void method instead of async Task . async-awaitParallel.ForEach 并不能真正起到很好的作用 ,因为它将异步lambda转换为async void方法,而不是async Task

What i would recommend is to drop the Parallel.ForEach and the wrapped sync call and do that following: 我建议您删除Parallel.ForEach和包装的同步调用,然后执行以下操作:

Change your method call from async to sync (as it really isn't async at all): 将您的方法调用从异步更改为同步(因为它实际上根本不是异步 ):

private ProductSearchResult RetrieveProductAvailablity(int productId,
                                                       CancellationToken
                                                       cancellationToken)

Instead of this: 而不是这个:

bool success = await Task.Run(() => app.RetrieveProductAvailability(productId));

Invoke the method call synchronously: 同步调用方法调用:

bool success = app.RetrieveProductAvailability(productId));

And then explicitly invoke Task.Run on all of them: 然后显式调用Task.Run对所有它们:

var productTasks = products.Select(product => Task.Run(() => 
                                   RetrieveProductAvailablity(product.ID, cts.Token))

await Task.WhenAll(productTasks);

Generally, it's not recommended to expose async wrappers over sync methods 通常,不建议在同步方法上公开异步包装

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM