#c# #.net #asynchronous #task-parallel-library #async-await
#c# #.net #асинхронный #task-parallel-library #async-await
Вопрос:
Я использую TPL и async / await для создания async API поверх webclient для своих приложений. И в нескольких местах (обычно там, где мне нужно запустить кучу асинхронных задач и дождаться их всех в конце), я следую фрагменту кода. Я просто хочу убедиться, что все правильно понял, поскольку, несмотря на то, что писать асинхронный код с помощью TPL относительно легко, а отладка / устранение неполадок async / await по-прежнему является сложной задачей (как интерактивная отладка, так и устранение неполадок на сайте заказчика) — поэтому я хочу сделать это правильно.
Моя цель: иметь возможность фиксировать исключения, сгенерированные из исходной задачи, задач продолжения, а также дочерних задач, чтобы я мог обработать это (если мне нужно). Я не хочу, чтобы какое-либо исключение было удалено и забыто.
Основные принципы, которые я использовал: 1. .net framework гарантирует, что исключение будет прикреплено к задаче 2. Блок Try / catch может быть применен к async / await, чтобы создать иллюзию / читаемость кода синхронизации (ссылки: http://channel9.msdn.com/Events/TechDays/Techdays-2014-the-Netherlands/Async-programming-deep-dive , http://blogs.msdn.com/b/ericlippert/archive/2010/11/19/asynchrony-in-c-5-part-seven-exceptions.aspx , http://msdn.microsoft.com/en-us/library/dd537614.aspx и т.д.)
Вопрос: Я хотел бы получить подтверждение того, что желаемая цель (что я могу фиксировать исключения из исходных, продолжения и дочерних задач) достигнута, и есть ли какие-либо улучшения, которые я могу внести в образец:
Например, будет ли случай, когда одна из составных задач (например, развернутая прокси-задача) вообще не будет активирована (состояние waitforactivation), так что waitall может просто дождаться запуска задачи? Насколько я понимаю, эти случаи никогда не должны происходить, поскольку задача продолжения всегда выполняется и возвращает задачу, которая была отслежена прокси-сервером с использованием wnwrap. Пока я следую аналогичному шаблону на всех уровнях и API, шаблон должен фиксировать все агрегированные исключения в связанных задачах.
Примечание: по сути, я ищу предложения, такие как предотвращение создания фиктивной задачи, которую я создаю в задаче продолжения, если статус исходной задачи не запущен до завершения, или использование прикрепления к родительскому элементу, чтобы я мог ждать только от родительского элемента и т.д. чтобы увидеть все возможности, чтобы я мог выбрать наилучший вариант, поскольку этот шаблон сильно зависит от обработки ошибок в моем приложении.
static void SyncAPIMethod(string[] args)
{
try
{
List<Task> composedTasks = new List<Task>();
//the underlying async method follow the same pattern
//either they chain the async tasks or, uses async/await
//wherever possible as its easy to read and write the code
var task = FooAsync();
composedTasks.Add(task);
var taskContinuation = task.ContinueWith(t =>
{
//Intentionally not using TaskContinuationOptions, so that the
//continuation task always runs - so that i can capture exception
//in case something is wrong in the continuation
List<Task> childTasks = new List<Task>();
if (t.Status == TaskStatus.RanToCompletion)
{
for (int i = 1; i <= 5; i )
{
var childTask = FooAsync();
childTasks.Add(childTask);
}
}
//in case of faulted, it just returns dummy task whose status is set to
//'RanToCompletion'
Task wa = Task.WhenAll(childTasks);
return wa;
});
composedTasks.Add(taskContinuation);
//the unwrapped task should capture the 'aggregated' exception from childtasks
var unwrappedProxyTask = taskContinuation.Unwrap();
composedTasks.Add(unwrappedProxyTask);
//waiting on all tasks, so the exception will be thrown if any of the tasks fail
Task.WaitAll(composedTasks.ToArray());
}
catch (AggregateException ag)
{
foreach(Exception ex in ag.Flatten().InnerExceptions)
{
Console.WriteLine(ex);
//handle it
}
}
}
Комментарии:
1. Этот вопрос, похоже, не по теме, потому что это вопрос проверки кода, который должен быть на codereview.stackexchange.com
2. @Eugene, я вроде бы знаю, что шаблон, который я использовал, скорее всего, работает, но цель состоит в том, чтобы убедиться и сделать его лучше. Например, в блоке продолжения я всегда создаю фиктивную задачу с помощью task when all, чтобы убедиться, что я могу зафиксировать ошибку развернутой задачи. то же самое для вложенных задач с помощью unwrap . Просто хочу убедиться, что я использую правильные расширения (например, лучше ли присоединять задачи продолжения и дочерние задачи к исходной задаче и просто ждать ее?)
3. ИМО, этот код мог бы быть намного проще и элегантнее с
async/await
. Я не понимаю причины, по которой вы придерживаетесьContinueWith
иUnwrap
, и почему вы добавляете как внутреннюю, так и внешнюю (развернутую) задачу вcomposedTasks
.4. @ Noseratio, везде, где я использую / могу использовать async / await в async api / method, я просто оборачиваю их в try / catch (и да, это намного лучше и элегантнее). Но обратите внимание, что это не метод ‘async’, а вызов ‘sync’ — есть некоторые API, которым все еще требуется поддержка syn api. Итак, для этого мне пришлось использовать continue / unwrap для составления всех задач и ожидания их в конце.
5. @Noseration, теперь я понял :). Однако у меня есть один вопрос, пожалуйста, смотрите ниже. С уважением.
Ответ №1:
Из комментариев:
ИМО, этот код мог бы быть намного проще и элегантнее с async / await. Я не понимаю причины, по которой вы придерживаетесь ContinueWith и Unwrap, и почему вы добавляете как внутреннюю, так и внешнюю (развернутую) задачу в composedTasks.
Я имел в виду что-то вроде приведенного ниже. Я думаю, что это делает то же самое, что и ваш исходный код, но без ненужной избыточности в виде composedTasks
, ContinueWith
и Unwrap
. Они вам почти никогда не понадобятся, если вы используете async/await
.
static void Main(string[] args)
{
Func<Task> doAsync = async () =>
{
await FooAsync().ConfigureAwait(false);
List<Task> childTasks = new List<Task>();
for (int i = 1; i <= 5; i )
{
var childTask = FooAsync();
childTasks.Add(childTask);
}
await Task.WhenAll(childTasks);
};
try
{
doAsync().Wait();
}
catch (AggregateException ag)
{
foreach (Exception ex in ag.Flatten().InnerExceptions)
{
Console.WriteLine(ex);
//handle it
}
}
}
static async Task FooAsync()
{
// simulate some CPU-bound work
Thread.Sleep(1000);
// we could have avoided blocking like this:
// await Task.Run(() => Thread.Sleep(1000)).ConfigureAwait(false);
// introduce asynchrony
// FooAsync returns an incomplete Task to the caller here
await Task.Delay(1000).ConfigureAwait(false);
}
Обновлено для устранения комментария:
есть несколько вариантов использования, когда я продолжаю после «создания дочерних задач» вызывать более «независимые» задачи.
В принципе, существует три распространенных сценария для любого рабочего процесса асинхронной задачи: последовательная композиция, параллельная композиция или любая комбинация этих двух (смешанная композиция):
-
последовательная композиция:
await task1; await task2; await task3;
-
параллельная композиция:
await Task.WhenAll(task1, task2, task3); // or await Task.WhenAny(task1, task2, task3);
-
смешанный состав:
var func4 = new Func<Task>(async () => { await task2; await task3; }); await Task.WhenAll(task1, func4());
Если какая-либо из вышеперечисленных задач работает с привязкой к процессору, вы можете использовать Task.Run
для этого, например:
var task1 = Task.Run(() => CalcPi(numOfPiDigits));
Где CalcPi
находится синхронный метод, выполняющий фактическое вычисление.
Комментарии:
1. 1 Ооо, круто — это намного лучше. Однако у меня есть один вопрос — в моей версии код гарантирует, что поток, какой бы ни был вызывающий, выполнит некоторую работу процессора, а затем будет ждать. Но с версией async / await в lamda, похоже, что поток, скорее всего, ожидает немедленного выполнения.
2. @Dreamer, как и в вашей версии, это полностью зависит от того, что внутри
FooAsync
. Вызывающий потокdoAsync
будет заблокирован до тех пор, пока где-то внутри не будет введена асинхронностьFooAsync
. Я обновил ответ, чтобы проиллюстрировать это, обратите внимание, чтоThread.Sleep(1000)
внутриFooAsync
это заполнитель для любой работы, связанной с процессором, которая у вас может быть.3. но при этом ‘doAsync’ просто возвращается с сгенерированной задачей в операторе «await». И он возобновляется, если асинхронная задача завершается из инструкции рядом с await, и начинается составление дочерних задач. Но, в то время как в моей версии в вопросе, как только fooasync возвращается, я все еще создаю и выполняю больше работы с ЦП, тогда как здесь он блокируется и выполняет больше работы с ЦП только после завершения первого ожидания в doasync.
4.@Dreamer, какую работу процессора ты имеешь в виду? Я не думаю, что вы делаете что-либо параллельно с
fooasync
в вашем коде, что касаетсяcomposedTasks
. Это потому, что все, что вы делаете внутри,ContinueWith
будет выполнено послеfooasync
завершения. По сути, только 1 задача из 3, добавленных вcomposedTasks
, является «горячей», в отличие от сchildTask
.5. о, это верно из примера — но есть некоторые варианты использования, когда я продолжаю после «создания дочерних задач» вызывать более «независимые» задачи. Но вы правы — я должен обновить вопрос. Позвольте мне подумать об этом и обновлю завтра (время спать). Если я смогу использовать шаблон ur во всех других случаях, я продолжу и приму его в качестве ответа завтра. Спасибо.