Как легко получить deadlock на Task.WhenAll

—

от автора

Напоминание! 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

Thread'ы выясняют, кто локнул ресурсы

Thread’ы выясняют, кто локнул ресурсы

Пишем какой-то 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 и всегда правильно запускайте свои таски.


ссылка на оригинал статьи https://habr.com/ru/articles/792788/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *