Как легко получить 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 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’ы выясняют, кто локнул ресурсы
Пишем какой-то producer-consumer с async\await внутри, даже добавили CancelationToken’ы и TaskCreationOptions, но забыли Task.Run.
Ловим deadlock и уже ничего нам не поможет, включая timeout;
async Task TestDeadlock()
{
var source = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var channel = Channel.CreateBounded(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 и всегда правильно запускайте свои таски.