我并行启动了一些异步任务,如以下示例所示:
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();
这是有效的方法吗?有这样的东西吗?
答案 0 :(得分:4)
我个人更喜欢问题中的代码(但出于代码可维护性的原因,请使用await
而不是Result
)。如andyb952's answer中所述,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)
);
但是它真的更具可读性吗?到目前为止,我还没有足够的说服力使其成为图书馆。
答案 1 :(得分:3)
在这种情况下,我认为,当您在之后立即使用结果时,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个任务
答案 2 :(得分:-2)
正如andyb952's answer中指出的,在这种情况下,由于all the tasks are hot and running,实际上不需要调用Task.WhenAll
。
但是,在某些情况下,您可能仍希望使用AsyncCollector
类型。
TL; DR:
Async
辅助功能usage example async Task Async(Func<Task> asyncDelegate) =>
await asyncDelegate().ConfigureAwait(false);
AsyncCollector
implementation,usage example 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.
解决方案
一种解决方法是根据任务的 index 分配值,当然,仅当任务以与项目相同的顺序创建(并存储)时有效:
await Task.WhenAll(tasks);
for (int i = 0; i < tasks.Count; i++)
translations[i].Value = await tasks[i];
一种更合适的解决方案是使用Linq
并生成一个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);
更清洁的解决方案 (恕我直言)
在这种情况下,我喜欢使用包装了异步操作的帮助程序功能(任何类型的操作),与上面的Select
创建任务的方式非常相似:< / p>
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.
Here,您可以找到使用此帮助器功能的完整示例,而无需注释的内容将变为:
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);
}
注意:如评论中所指出,使用闭包和枚举器会涉及风险,但是from C# 5 onwards the use of foreach
is safe是因为闭包每次都会关闭新的变量副本。
如果您仍然想在C#的早期版本中使用此类型,并且在关闭过程中需要安全性,可以更改Register
方法以接受 subject 可以在委托内部使用,避免闭包。
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();