[英]How to bundle async tasks for Task.WhenAll?
我並行地啟動了一些異步任務,例如以下示例:
var BooksTask = _client.GetBooks(clientId);
var ExtrasTask = _client.GetBooksExtras(clientId);
var InvoicesTask = _client.GetBooksInvoice(clientId);
var ReceiptsTask = _client.GetBooksRecceipts(clientId);
await Task.WhenAll(
BooksTask,
ExtrasTask,
InvoicesTask,
ReceiptsTask
);
model.Books = BooksTask.Result;
model.Extras = ExtrasTask.Result;
model.Invoices = InvoicesTask.Result;
model.Receipts = ReceiptsTask.Result;
這導致很多輸入。 我在.Net Framework中搜索了一種縮短時間的方法。 我想這很卑鄙。 我將其稱為“ Collector
”類,因為我不知道如何命名該概念。
var collector = new Collector();
collector.Bind(_client.GetBooks(clientId), out model.Books);
collector.Bind(_client.GetBooksExtras(clientId), out model.Extras);
collector.Bind(_client.GetBooksInvoice(clientId), out model.Invoices);
collector.Bind(_client.GetBooksRecceipts(clientId), out model.Receipts);
collector.Run();
這是有效的方法嗎? 有那樣的東西嗎?
就個人而言,我更喜歡問題中的代碼(但出於代碼可維護性的原因,請使用await
而不是Result
)。 如在andyb952的答案中所述, Task.WhenAll
。 出於可讀性原因,我更喜歡它; 它使語義明確,而IMO使代碼更易於閱讀。
我在.Net Framework中搜索了一種縮短時間的方法。
沒有內置的東西,(據我所知)也沒有任何庫。 我已經考慮過使用元組編寫一個。 對於您的代碼,它看起來像這樣:
public static class TaskHelpers
{
public static async Task<(T1, T2, T3, T4)> WhenAll<T1, T2, T3, T4>(Task<T1> task1, Task<T2> task2, Task<T3> task3, Task<T4> task4)
{
await Task.WhenAll(task1, task2, task3, task4).ConfigureAwait(false);
return (await task1, await task2, await task3, await task4);
}
}
有了此輔助程序,您的原始代碼將簡化為:
(model.Books, model.Extras, model.Invoices, model.Receipts) = await TaskHelpers.WhenAll(
_client.GetBooks(clientId),
_client.GetBooksExtras(clientId),
_client.GetBooksInvoice(clientId),
_client.GetBooksRecceipts(clientId)
);
但這真的更具可讀性嗎? 到目前為止,我還沒有足夠的說服力使其成為圖書館。
在這種情況下,我認為,當您在緊接之后使用結果時,WhenAll是無關緊要的。 更改為此將具有相同的效果。
var BooksTask = _client.GetBooks(clientId);
var ExtrasTask = _client.GetBooksExtras(clientId);
var InvoicesTask = _client.GetBooksInvoice(clientId);
var ReceiptsTask = _client.GetBooksRecceipts(clientId);
model.Books = await BooksTask;
model.Extras = await ExtrasTask;
model.Invoices = await InvoicesTask;
model.Receipts = await ReceiptsTask;
等待者將確保您在完成所有任務之前不會超過后面的4個任務
正如在andyb952的答案中指出的 ,在這種情況下,實際上不需要調用Task.WhenAll
因為所有任務都是熱的並且正在運行 。
但是,在某些情況下,您可能仍希望擁有AsyncCollector
類型。
TL; DR:
Async
助手功能用法示例 async Task Async(Func<Task> asyncDelegate) =>
await asyncDelegate().ConfigureAwait(false);
var collector = new AsyncCollector();
collector.Register(async () => model.Books = await _client.GetBooks(clientId));
collector.Register(async () => model.Extras = await _client.GetBooksExtras(clientId));
collector.Register(async () => model.Invoices = await _client.GetBooksInvoice(clientId));
collector.Register(async () => model.Receipts = await _client.GetBooksReceipts(clientId));
await collector.WhenAll();
如果您擔心關閉,請參閱結尾處的注釋。
讓我們看看為什么有人想要那樣。
這是同時運行任務的解決方案:
var task1 = _client.GetFooAsync();
var task2 = _client.GetBarAsync();
// Both tasks are running.
var v1 = await task1;
var v2 = await task2;
// It doesn't matter if task2 completed before task1:
// at this point both tasks completed and they ran concurrently.
問題
當您不知道要使用多少個任務時該怎么辦?
在這種情況下,您不能在編譯時定義任務變量。
僅將任務存儲在集合中並不能解決問題,因為每個任務的結果都應分配給特定變量!
var tasks = new List<Task<string>>();
foreach (var translation in translations)
{
var translationTask = _client.TranslateAsync(translation.Eng);
tasks.Add(translationTask);
}
await Task.WhenAll(tasks);
// Now there are N completed tasks, each with a value that
// should be associated to the translation instance that
// was used to generate the async operation.
解決方案
一種解決方法是根據任務的索引分配值,這當然只有在任務以與項目相同的順序創建(和存儲)時才有效:
await Task.WhenAll(tasks);
for (int i = 0; i < tasks.Count; i++)
translations[i].Value = await tasks[i];
一個更合適的解決方案是使用Linq
並生成一個Task
,該Task
標識兩個操作:數據的獲取和對其接收者的分配
List<Task> translationTasks = translations
.Select(async t => t.Value = await _client.TranslateAsync(t.Eng))
// Enumerating the result of the Select forces the tasks to be created.
.ToList();
await Task.WhenAll(translationTasks);
// Now all the translations have been fetched and assigned to the right property.
這看起來不錯,直到您需要在另一個列表或另一個單個值上執行相同的模式,然后您的函數中開始需要管理許多List<Task>
和Task
:
var translationTasks = translations
.Select(async t => t.Value = await _client.TranslateAsync(t.Eng))
.ToList();
var fooTasks = foos
.Select(async f => f.Value = await _client.GetFooAsync(f.Id))
.ToList();
var bar = ...;
var barTask = _client.GetBarAsync(bar.Id);
// Now all tasks are running concurrently, some are also assigning the value
// to the right property, but now the "await" part is a bit more cumbersome.
bar.Value = await barTask;
await Task.WhenAll(translationTasks);
await Task.WhenAll(fooTasks);
更清潔的解決方案 (imho)
在這種情況下,我喜歡使用包裝異步操作 (任何類型的操作) 的輔助函數,該函數與上面的“ Select
創建任務的方式非常相似:
async Task Async(Func<Task> asyncDelegate) =>
await asyncDelegate().ConfigureAwait(false);
在以前的場景中使用此功能將導致以下代碼:
var tasks = new List<Task>();
foreach (var t in translations)
{
// The fetch of the value and its assignment are wrapped by the Task.
var fetchAndAssignTask = Async(async t =>
{
t.Value = await _client.TranslateAsync(t.Eng);
});
tasks.Add(fetchAndAssignTask);
}
foreach (var f in foos)
// Short syntax
tasks.Add(Async(async f => f.Value = await _client.GetFooAsync(f.Id)));
// It works even without enumerables!
var bar = ...;
tasks.Add(Async(async () => bar.Value = await _client.GetBarAsync(bar.Id)));
await Task.WhenAll(tasks);
// Now all the values have been fetched and assigned to their receiver.
在這里,您可以找到使用此幫助器功能的完整示例,該示例無需注釋即可:
var tasks = new List<Task>();
foreach (var t in translations)
tasks.Add(Async(async t => t.Value = await _client.TranslateAsync(t.Eng)));
foreach (var f in foos)
tasks.Add(Async(async f => f.Value = await _client.GetFooAsync(f.Id)));
tasks.Add(Async(async () => bar.Value = await _client.GetBarAsync(bar.Id)));
await Task.WhenAll(tasks);
AsyncCollector類型
此技術可以輕松地包裝在“ Collector
”類型內:
class AsyncCollector
{
private readonly List<Task> _tasks = new List<Task>();
public void Register(Func<Task> asyncDelegate) => _tasks.Add(asyncDelegate());
public Task WhenAll() => Task.WhenAll(_tasks);
}
注意:正如評論中指出的那樣,使用閉包和枚舉器會涉及風險,但是從C#5開始,使用foreach
是安全的,因為閉包每次都會關閉新的變量副本。
如果您仍然想在C#的早期版本中使用此類型,並且在關閉過程中需要安全,可以更改Register
方法,以接受將在委托內部使用的主題 ,從而避免關閉。
public void Register<TSubject>(TSubject subject, Func<TSubject, Task> asyncDelegate)
{
var task = asyncDelegate(subject);
_tasks.Add(task);
}
然后,代碼變為:
var collector = new AsyncCollector();
foreach (var translation in translations)
// Register translation as a subject, and use it inside the delegate as "t".
collector.Register(translation,
async t => t.Value = await _client.TranslateAsync(t.Eng));
foreach (var foo in foos)
collector.Register(foo, async f.Value = await _client.GetFooAsync(f.Id));
collector.Register(bar, async b => b.Value = await _client.GetBarAsync(bar.Id));
await collector.WhenAll();
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.