繁体   English   中英

如何为Task.WhenAll捆绑异步任务?

[英]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 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.

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