Фиксирует ли приведенный ниже код исключения из исходных, продолжающихся и дочерних задач в TPL?

#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 во всех других случаях, я продолжу и приму его в качестве ответа завтра. Спасибо.