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

Threads find out who locked resources

Threads find out who locked resources

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *