[英]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包装异步,我有两个问题。
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-await
和Parallel.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.