Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task.Run continues on the same thread causing deadlock

Consider the following async method that I'm going to wait synchronously. Wait a second, I know. I know that it's considered bad practice and causes deadlocks, but I'm fully conscious of that and taking measures to prevent deadlocks via wrapping code with Task.Run.

    private async Task<string> BadAssAsync()
    {
        HttpClient client = new HttpClient();

        WriteInfo("BEFORE AWAIT");

        var response = await client.GetAsync("http://google.com");

        WriteInfo("AFTER AWAIT");

        string content = await response.Content.ReadAsStringAsync();

        WriteInfo("AFTER SECOND AWAIT");

        return content;
    }

This code will definitely deadlock (in environments with SyncronizationContext that schedules tasks on a single thread like ASP.NET) if called like that: BadAssAsync().Result.

The problem I face is that even with this "safe" wrapper it still occasionally deadlocks.

    private T Wait1<T>(Func<Task<T>> taskGen)
    {
        return Task.Run(() =>
        {
            WriteInfo("RUN");

            var task = taskGen();

            return task.Result;
        }).Result;
    }

These "WriteInfo" lines there in purpose. These debug lines allowed me to see that the reason why it occasionally happens is that the code within Task.Run, by some mystery, is executed by the very same thread that started serving request. It means that is has AspNetSynchronizationContext as SyncronizationContext and will definitely deadlock.

Here is debug output:

*** (worked fine)
START: TID: 17; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
RUN: TID: 45; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
BEFORE AWAIT: TID: 45; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
AFTER AWAIT: TID: 37; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
AFTER SECOND AWAIT: TID: 37; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler

*** (deadlocked)
START: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
RUN: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
BEFORE AWAIT: TID: 48; SCTX: System.Web.AspNetSynchronizationContext; SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler

Notice as code within Task.Run() continues on the very same thread with TID=48.

The question is why is this happening? Why Task.Run runs code on the very same thread allowing SyncronizationContext to still have an effect?

Here is the full sample code of WebAPI controller: https://pastebin.com/44RP34Ye and full sample code here.

UPDATE. Here is the shorter Console Application code sample that reproduces root cause of the issue -- scheduling Task.Run delegate on the calling thread that waits. How is that possible?

static void Main(string[] args)
{
    WriteInfo("\n***\nBASE");

    var t1 = Task.Run(() =>
    {
        WriteInfo("T1");

        Task t2 = Task.Run(() =>
        {
            WriteInfo("T2");
        });

        t2.Wait();
    });

    t1.Wait();
}
BASE: TID: 1; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
T1: TID: 3; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
T2: TID: 3; SCTX: &ltnull> SCHEDULER: System.Threading.Tasks.ThreadPoolTaskScheduler
like image 825
Eugene D. Gubenkov Avatar asked Sep 29 '17 16:09

Eugene D. Gubenkov


People also ask

What is task deadlock?

Deadlocks occur when multiple tasks or threads cannot make progress because each task is waiting for a lock held by another task that is also stuck.

What causes deadlocks in C#?

Deadlock occurs when a resource is locked by a thread and is required by another thread at the same time. This problem occur frequenty in a multiprocessing system. Thread One will not get Lock Q since it belongs to Thread Two.

What is the difference between task run and task factory StartNew?

Task. Run(action) internally uses the default TaskScheduler , which means it always offloads a task to the thread pool. StartNew(action) , on the other hand, uses the scheduler of the current thread which may not use thread pool at all!

How can you avoid deadlock in threading C#?

The simplest way to avoid deadlock is to use a timeout value. The Monitor class (system. Threading. Monitor) can set a timeout during acquiring a lock.


1 Answers

We with a good friend of mine were able to figure this one out via inspecting stack traces and reading .net reference source. It's evident that the root cause of problem is that Task.Run's payload is being executed on the thread that calls Wait on the task. As it turned out this is a performance optimization made by TPL in order not to spin up extra threads and prevent precious thread from doing nothing.

Here is an article by Stephen Toub that describes the behavior: https://blogs.msdn.microsoft.com/pfxteam/2009/10/15/task-wait-and-inlining/.

Wait could simply block on some synchronization primitive until the target Task completed, and in some cases that’s exactly what it does. But blocking threads is an expensive venture, in that a thread ties up a good chunk of system resources, and a blocked thread is dead weight until it’s able to continue executing useful work. Instead, Wait prefers to execute useful work rather than blocking, and it has useful work at its finger tips: the Task being waited on. If the Task being Wait’d on has already started execution, Wait has to block. However, if it hasn’t started executing, Wait may be able to pull the target task out of the scheduler to which it was queued and execute it inline on the current thread.

Lesson: If you really need to synchronously wait asynchronous work the trick with Task.Run is not reliable. You have to zero out SyncronizationContext, wait, and then return SyncronizationContext back.

like image 103
Eugene D. Gubenkov Avatar answered Sep 28 '22 04:09

Eugene D. Gubenkov