9

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
Eugene D. Gubenkov
  • 5,127
  • 6
  • 39
  • 71
  • Your approach doesn't help at all. – SLaks Sep 29 '17 at 16:05
  • 1
    `taking measures to prevent deadlocks via wrapping code with Task.Run.` And as you're seeing, that doesn't solve your problem. You shouldn't be synchronously waiting on asynchronous operations. It's *inherently* problematic. There is no "simple trick" that just makes it okay. You need to remove that underlying problem, and either make your code entirely asynchronous, or entirely synchronous, if you don't want to have to be continually dealing with problems like these. – Servy Sep 29 '17 at 16:06
  • Tasks run asynchronously, not in parallel. A task will always be run on the same thread that spawns it, which is why your locking is still a problem. See this post for the difference between parallelism and asynchronous https://stackoverflow.com/questions/4844637/what-is-the-difference-between-concurrency-parallelism-and-asynchronous-methods – Neil Sep 29 '17 at 16:07
  • 1
    @NeilBostian: "Task"s themselves do not "run", except for `Task.Run`, which does run on a background thread. You're thinking of calls to async methods. – SLaks Sep 29 '17 at 16:08
  • 2
    @NeilBostian `Task` are a representation of asynchronous work. They may or may not end up doing that work in parallel, based on how you use them, and how they are implemented. The statement "A task will always be run on the same thread that spawns it" doesn't even make sense. Many tasks don't involve running code in a thread at all, and those that do *typically* don't involve running code in the calling thread (because they're supposed to be asynchronous). Your linked answer doesn't use its terms properly, as it claims work isn't being done in parallel when work is being done in parallel. – Servy Sep 29 '17 at 16:12
  • @Servy I definitely disagree with this statement "Many tasks don't involve running code in a thread at all." but as @SLaks pointed out already `Task.Run` will be on a separate thread where a typical async call wouldn't be. – Neil Sep 29 '17 at 16:17
  • You could (and should) of course make your [HttpGet] method async... That's the only sensible way. Stephen Cleary wrote about ["turtles all the way down"](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx) , do read that. – H H Sep 29 '17 at 16:55
  • @NeilBostian The OP's question itself has examples of asynchronous operations that don't represent executing code in a thread, namely they represent performing network IO requests, which aren't going to require running code on a thread to do their work. `Task.Run` is actually a rare exception in that it really is an asynchronous operation that inherently represents doing work on a thread (in that case, a thread pool thread). – Servy Sep 29 '17 at 16:58
  • 3
    @SLaks, "Your approach doesn't help at all" - thanks for noticing that! The question is why? – Eugene D. Gubenkov Sep 29 '17 at 18:34
  • Blocking on a `Task` from `Task.Run` is no different from blocking on any other `Task`. Your entire problem is almost unsolvable; you need to make your entire callchain async. – SLaks Sep 29 '17 at 18:43
  • @SLaks, that's is not true, is it? The problem can be solved by setting the SyncronizationContext to null for instance. Also, as it stated there: https://stackoverflow.com/questions/28305968/use-task-run-in-synchronous-method-to-avoid-deadlock-waiting-on-async-method the problem should be solved by Task.Run as well. I guess what I'm really trying to understand is that why the request thread is used to run delegate I've passed to Task.Run instead of other free thread pool thread? – Eugene D. Gubenkov Sep 29 '17 at 18:46
  • 3
    Excellent question! @All: the question is not about "good practices", it's about inner workings of TPL. – Vlad Sep 30 '17 at 09:22
  • An interesting finding: Almost identical trick I've found in IdentityServer3 internals: https://github.com/IdentityServer/IdentityServer3.AccessTokenValidation/blob/master/source/AccessTokenValidation/Plumbing/AsyncHelper.cs – Eugene D. Gubenkov Oct 23 '18 at 11:59

2 Answers2

3

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.

Eugene D. Gubenkov
  • 5,127
  • 6
  • 39
  • 71
  • 3
    IMO, the lesson is that tricks are never reliable. If you want manual thread management, so do it explicitly and not (attempt to) rely on other methods' side effects. – Vlad Sep 30 '17 at 09:29
3

In the internals of IdentityServer I found another technique that works. It is very close to not reliable technique laid down in the the question. You will find the source code at the end or this answer.

Credits for the magic of this technique should go to Unwrap() method. Internally when called against Task<Task<T>> it creates a new "promise task" that completes as soon as both (the one we executing against and the nested one) tasks complete.

The reason why this works out and does not create probability of deadlock is simple -- promise tasks are no subjects for inlining and that makes sense as there is no "work" to inline. In turn that means that we blocking current thread and let default scheduler (ThreadPoolTaskScheduler) do the work in the new thread in absence of SynchronizationContext.

internal static class AsyncHelper
{
    private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

    public static void RunSync(Func<Task> func)
    {
        _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
    }

    public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
    }
}

Moreover, there is a signature of Task.Run that does Unwrap implicitly which leads to the shortest safe implementation below.

T SyncWait<T>(Func<Task<T>> f) => Task.Run(f).Result;
Eugene D. Gubenkov
  • 5,127
  • 6
  • 39
  • 71