How to easily get a deadlock on Task.WhenAll
Reminder! Task.WhenAll does not give your tasks to the scheduler and if you forgot Task.Run or Task.Factory.StartNew, then welcome to synchronous execution and/or execution in main and/or catching deadlock.
Below are a couple of examples in which you can avoid this, but you shouldn’t do it.
whole code
Deadlock and Task.WhenAll. Don’t forget to use Task.Run or Task,Factory.StartNew (github.com)
Synchronously executed in main
A configurable method that will help us test several different situations
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);
}
Both tasks are synchronous
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.
Both tasks on the bullet
The first to go was Delay, the second after Yield. But you don’t have to do that!
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
One on the bullet, the other synchronously
The first one left after a very short Delay, the second one with 0 Delay and ConfigureAwait(false) was executed synchronously. And you don’t need to do that either!
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.
Always run your tasks using Task.Run or Task.Factory.StartNew.
There is, of course, an option with Task.Start(), but the top two are much more convenient and flexible.
From the launches it is clear that out pid main sometimes differs from in pid main, that is, in complex applications, the probability of launching on main is lower, but this can be synchronous.
Now let’s catch the deadlock
We are writing some kind of producer-consumer with async\await inside, we even added CancelationTokens and TaskCreationOptions, but forgot Task.Run.
We catch a deadlock and nothing will help us, including 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);
}
This example shows that the two tests above TestDelayAndYield And TestSmallDelayAndConfigureAwaitForZero it is not necessary that after await they will go into the pool and you should not rely on this behavior.
Don’t count on undefined behavior and always run your tasks correctly.