Напоминание! Task.WhenAll не отдает ваши задачи планировщику и если вы забыли Task.Run или Task.Factory.StartNew, то добро пожаловать на синхронное выполнение и\или выполнение в main и\или ловите deadlock.
А ниже пара примеров, при которых вы можете этого избежать, но так делать не надо.
код целиком
Deadlock and Task.WhenAll. Don't forget to use Task.Run or Task,Factory.StartNew (github.com)
Синхронно выполняемся в main
Конфигурируемый метод, который поможет нам протестировать несколько разных ситуаций
async Task<int> MethodAsync(int taskId, int sleepMs, int delayMs = 0, bool safeCtx = true, bool yield = false) { var taskIdStr = $"tid: {taskId,2}, "; var taskInfo = $"sleep: {sleepMs}, delay: {delayMs}, safeCtx: {safeCtx,5}, yield: {yield}"; PrintPid(true, Scope.Task, taskIdStr + taskInfo); if (yield) { await Task.Yield(); } else { await Task.Delay(delayMs).ConfigureAwait(safeCtx); } Thread.Sleep(sleepMs); PrintPid(false, Scope.Task, taskIdStr); return (int)Math.Sqrt(sleepMs); }
Обе таски синхронно
async Task TestSync() { var task0 = MethodAsync(0, 1000); var task1 = MethodAsync(1, 2000); await Task.WhenAll(task0, task1); }
in (Main) pid: 4. in (Test) pid: 4. TestSync in (Task) pid: 4. tid: 0, sleep: 1000, delay: 0, safeCtx: True, yield: False out (Task) pid: 4. tid: 0, in (Task) pid: 4. tid: 1, sleep: 2000, delay: 0, safeCtx: True, yield: False out (Task) pid: 4. tid: 1, out (Test) pid: 4. total sleep: 3,007 ms out (Main) pid: 4.
Обе таски на пуле
Первая ушла Delay, вторая после Yield. Но так делать не надо!
async Task TestDelayAndYield() { var task0 = MethodAsync(0, 1000, 10); var task1 = MethodAsync(1, 2000, yield: true); await Task.WhenAll(task0, task1); }
in (Main) pid: 8. in (Test) pid: 8. TestDelayAndYield in (Task) pid: 8. tid: 0, sleep: 1000, delay: 10, safeCtx: True, yield: False in (Task) pid: 8. tid: 1, sleep: 2000, delay: 0, safeCtx: True, yield: True out (Task) pid: 0. tid: 0, out (Task) pid: 10. tid: 1, out (Test) pid: 10. total sleep: 2,014 ms out (Main) pid: 1
Одна на пуле, другая синхронно
Первая ушла после очень короткого Delay, вторая при 0 Delay и ConfigureAwait(false) выполнилась синхронно. И так делать тоже не надо!
async Task TestSmallDelayAndConfigureAwaitForZero() { var task0 = MethodAsync(0, 1000, 1); var task1 = MethodAsync(1, 2000, 0, safeCtx: false); await Task.WhenAll(task0, task1); }
in (Main) pid: 4. in (Test) pid: 4. TestSmallDelayAndConfigureAwaitForZero in (Task) pid: 4. tid: 0, sleep: 1000, delay: 1, safeCtx: True, yield: False in (Task) pid: 4. tid: 1, sleep: 2000, delay: 0, safeCtx: False, yield: False out (Task) pid: 15. tid: 0, out (Task) pid: 4. tid: 1, out (Test) pid: 4. total sleep: 2,019 ms out (Main) pid: 4
Use the Task.Run, Luke!
async Task TestTaskRun() { var task0 = Task.Run(() => MethodAsync(0, 1000)); var task1 = Task.Factory.StartNew(() => MethodAsync(1, 2000)); await Task.WhenAll(task0, task1); }
in (Main) pid: 4. in (Test) pid: 4. TestTaskRun in (Task) pid: 5. tid: 0, sleep: 1000, delay: 0, safeCtx: True, yield: False in (Task) pid: 0. tid: 1, sleep: 2000, delay: 0, safeCtx: True, yield: False out (Task) pid: 5. tid: 0, out (Task) pid: 0. tid: 1, out (Test) pid: 0. total sleep: 2,016 ms out (Main) pid: 0.
Всегда запускайте свои таски используя Task.Run или Task.Factory.StartNew.
Есть еще, конечно, вариант с Task.Start(), но верхние два куда более удобные и гибкие.
Из запусков видно, out pid main иногда отличается от in pid main, то есть в сложных приложениях, вероятность запуститься на main ниже, но это может быть синхронно.
А теперь ловим deadlock

Пишем какой-то producer-consumer с async\await внутри, даже добавили CancelationToken'ы и TaskCreationOptions, но забыли Task.Run.
Ловим deadlock и уже ничего нам не поможет, включая timeout;
async Task TestDeadlock() { var source = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var channel = Channel.CreateBounded<int>(100); var writeTask = new Task(async () => // Task.Run(async () => { try { foreach (var i in Enumerable.Range(0, 10000)) { await channel.Writer.WriteAsync(i, source.Token); } } catch (Exception ex) { Console.WriteLine("U think u can exit by timeout? But u got lock on main thread"); } finally { channel.Writer.TryComplete(); } }, TaskCreationOptions.PreferFairness | TaskCreationOptions.LongRunning); var readTask = new Task(async () => // Task.Run(async () => { try { var sum = 0; Console.Write("calc sum"); while (await channel.Reader.WaitToReadAsync(source.Token)) { var i = await channel.Reader.ReadAsync(source.Token); sum += i; Console.Write(new string('.', (i % 3)+1).PadRight(3)); Console.SetCursorPosition(Console.CursorLeft - 3, Console.CursorTop); } Console.WriteLine(); Console.WriteLine($"sum: {sum}"); } catch (Exception ex) { Console.WriteLine("U think u can exit by timeout? But u got lock on main thread"); } }, TaskCreationOptions.PreferFairness | TaskCreationOptions.LongRunning); await Task.WhenAll(writeTask, readTask); }
Этот пример показывает, что два теста выше TestDelayAndYield и TestSmallDelayAndConfigureAwaitForZero не обязательно после await уйдут в пул и полагаться на это поведение не стоит.
Не рассчитывайте на undefined behavior и всегда правильно запускайте свои таски.
