Как связать асинхронные задачи для 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 способ сократить это. Я думаю, что это lile это. Я называю класс 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();

Это верный подход? Есть ли что-то подобное?

🤔 А знаете ли вы, что...
C# поддерживает асинхронное и параллельное программирование с помощью ключевых слов async и await.


451
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 последующих заданий, пока все задания не будут выполнены.


Как указано в ответ 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 и сгенерировать a 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 выше:

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);

Тип асинкколлектора

Эту технику можно легко обернуть внутри типа "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();

Решено

Лично я предпочитаю код в вопросе (но использую await вместо Result по соображениям удобства сопровождения кода). Как указано в ответ andyb952, Task.WhenAll не требуется. Я предпочитаю его из соображений удобочитаемости; это делает семантику явной, а IMO упрощает чтение кода.

I searched the .Net Framework for a way to shorten this up.

Для этого нет ничего встроенного и (насколько мне известно) каких-либо библиотек. Я думал о написании одного с использованием кортежей. Для вашего кода это будет выглядеть так:

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)
);

Но действительно ли это более читабельно? До сих пор я не был достаточно убежден, чтобы превратить это в библиотеку.