Разница между ожидающей задачей и await Task.WhenAll

#c# #asynchronous #async-await #task-parallel-library

#c# #асинхронный #async-ожидание #задача-параллельная-библиотека

Вопрос:

В чем разница при использовании await для нескольких ожидающих задач по сравнению с ожиданием завершения всех задач. Я понимаю, что сценарий 2 лучше с точки зрения производительности, потому что как asyncTask1, так и asyncTask2 предварительно формируются параллельно.

сценарий 1 :

 async Task Task()
{
   await asyncTask1();
   await asyncTask2();
}
  

сценарий 2 :

 async Task Task()
{
    t1 = asyncTask1();
    t2 =  asyncTask2();
    await Task.WhenAll(createWorkflowtask, getTaskWorkflowTask);
}
  

Комментарии:

1. Учитывая, что вы сами описали разницу, почему вы спрашиваете нас, в чем разница? Вы уже знаете.

2. @servry, я думал, что это сценарий, который приведет к повышению производительности, но не уверен, что это охватывает все крайние случаи. или если есть сценарий, в котором для коротких запущенных задач . использование параллельного подхода — не лучший вариант.

3. Последствия для производительности будут зависеть от огромного количества факторов. Разница между двумя частями кода заключается в том, что первая выполняет операции последовательно, вторая — параллельно.

4. Полезная информация о aysnc / await: learn.microsoft.com/en-us/dotnet/csharp/programming-guide /…

Ответ №1:

В сценарии 1 задачи выполняются последовательно (asyncTask1 должен завершиться до запуска asyncTask2), в то время как в сценарии 2 две задачи могут выполняться параллельно.

Комментарии:

1. О, тогда разница ОГРОМНА

Ответ №2:

Вы написали: «… потому что как asyncTask1, так и asyncTask2 предварительно формируются параллельно.»

Нет, это не так!

Дополнение: Ниже я писал, что все в async-await выполняется одним потоком. Шнайдер правильно прокомментировал, что в async-await может быть задействовано несколько потоков. Смотрите дополнение в конце.

Статья, которая очень помогла мне понять, как работает async-await, была этим интервью с Эриком-Липпертом, который сравнил async / await с поваром, готовящим ужин. (Где-то на полпути найдите async-await).

Эрик Липперт объясняет, что если повар начинает что-то делать и через некоторое время обнаруживает, что ему больше нечего делать, кроме как ждать завершения процесса, этот повар оглядывается вокруг, чтобы посмотреть, может ли он сделать что-то еще вместо ожидания.

При использовании async / await все еще задействован один поток. Этот поток может выполнять только одно действие одновременно. Пока поток занят выполнением задачи 1, он не может выполнить задачу 2. Только если он обнаружит ожидание в Task1, он начнет выполнять инструкции из Task2. В вашем сценарии 2 задачи не выполняются параллельно.

Однако между сценариями есть разница. В сценарии 1 первая инструкция задачи 2 не будет выполнена до полного завершения задачи 1. Сценарий 2 начнет выполнение первых инструкций task2, как только task1 встретит ожидание.

Если вы действительно хотите, чтобы task2 что-то делал, в то время как task1 также что-то делает, вам придется начать выполнять task2 в отдельном потоке. Простым способом сделать это в вашем сценарии было бы:

 var task1 = Task.Run( () => asyncTask1())
// this statement is executed while task1 begins executing on a different thread.
// hence this thread is free to do other things, like performing statements
// from task2:
var task2 = asyncTask();
// the following statement will only be executed if task2 encounters an await
DoSomethingElse();
// when we need results from both task1 and task2:
await Task.WhenAll(new Task[] {task1, task2});
  

Поэтому обычно лучше всего ожидать завершения задачи только в том случае, если вам нужен результат этой задачи. Пока вы можете делать другие вещи, делайте эти другие вещи, они будут выполняться, как только другая задача начнет ожидать, пока вы не начнете ожидать, и в этом случае ваш вызывающий начнет делать что-то, пока его не ждут и т.д.

Преимущество этого метода над параллельным выполнением многократно:

  • Все выполняется одним потоком: нет необходимости в мьютексах, нет шансов на взаимоблокировку, «голодание» и т. Д
  • Ваш код выглядит довольно последовательным. Сравните это с кодом, который использует Task.ContinueWith и аналогичные инструкции
  • Нет накладных расходов на запуск отдельного потока / запуск потока из пула потоков

Дополнение: комментарий Шнайдера ниже о нескольких потоках верен.

Некоторые тесты показали мне, что идентификатор потока текущего потока в ожидаемых задачах отличается от идентификатора потока вызывающего потока.

Новичкам в async-await важно понимать, что, хотя задействованы разные потоки, async-await не означает, что задачи выполняются параллельно. Если вы хотите параллелизма, вы специально должны указать, что задача должна выполняться параллельно.

Похоже, что повар в аналогии Эрика Липперта на самом деле является командой поваров, которые постоянно оглядываются вокруг, чтобы посмотреть, могут ли они помочь кому-то из других поваров, вместо того, чтобы ждать завершения своих задач. И действительно, если кук Альберт увидит ожидание и начнет делать что-то еще, кук Бернард может завершить задачу кука Альберта.

Комментарии:

1. «При использовании async / await все еще задействован один поток …» — Я думаю, что этот абзац вводит в заблуждение и / или сбивает с толку. Во многих сценариях, вероятно, будет задействовано несколько потоков. Существует один из логических потоков выполнения , который не следует путать с одним потоком . «Пока поток занят выполнением задачи 1, он не может выполнить задачу 2». — это не причина, по которой задача 2 не выполняется одновременно с задачей 1 — это потому, что логический поток кода диктует это. Задача 2 выполняется в невидимом продолжении, которое следует за задачей 1, возможно, выполняется в другом потоке.

Ответ №3:

В первом сценарии вы запускаете задачу, а затем ждете ее завершения, затем переходите ко второму и ждете завершения перед выходом из метода.
Во втором сценарии вы запускаете две задачи параллельно, а затем ждете, пока они не будут завершены при вызове Task.WhenAll

Ответ №4:

Используя Task.WhenAll

  • Вы не можете использовать методы с возвращаемыми типами, если все возвращаемые типы не совпадают и не используют универсальную версию WhenAll метода
  • У вас нет контроля над последовательностями, которые выполняли методы, потому что эти методы выполняются параллельно
  • Необработанное исключение в одном методе не прерывает выполнение других методов (из-за параллельной природы)

Из MSDN

Если какая-либо из предоставленных задач завершается с ошибкой, возвращенная задача также завершится в состоянии TaskStatus.Состояние с ошибкой, в котором его исключения будут содержать агрегацию набора развернутых исключений из каждой из предоставленных задач.

Использование await для нескольких методов

  • У вас есть контроль над последовательностями, которые вызываются функциями

  • Вы можете использовать разные возвращаемые типы и использовать эти возвращаемые типы на следующих шагах

  • Необработанное исключение в одном методе приведет к прерыванию выполнения других методов