20

Consider the following (based on the default MVC template), which is a simplified version of some "stuff" that happens in the background - it completes fine, and shows the expected result, 20:

public ActionResult Index()
{
    var task = SlowDouble(10);
    string result;
    if (task.Wait(2000))
    {
        result = task.Result.ToString();
    }
    else
    {
        result = "timeout";
    }

    ViewBag.Message = result;
    return View();
}
internal static Task<long> SlowDouble(long val)
{
    TaskCompletionSource<long> result = new TaskCompletionSource<long>();
    ThreadPool.QueueUserWorkItem(delegate
    {
        Thread.Sleep(50);
        result.SetResult(val * 2);
    });
    return result.Task;
}

However, now if we add some async into the mix:

public static async Task<long> IndirectSlowDouble(long val)
{
    long result = await SlowDouble(val);

    return result;
}

and change the first line in the route to:

var task = IndirectSlowDouble(10);

then it does not work; it times out instead. If we add breakpoints, the return result; in the async method only happens after the route has already completed - basically, it looks like the system is unwilling to use any thread to resume the async operation until after the request has finished. Worse: if we had used .Wait() (or accessed .Result), then it will totally deadlock.

So: what is with that? The obvious workaround is "don't involve async", but that is not easy when consuming libraries etc. Ultimately, there is no functional difference between SlowDouble and IndirectSlowDouble (although there is obvious a structural difference).

Note: the exact same thing in a console / winform / etc will work fine.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    does `await SlowDouble(val).ConfigureAwait(false)` inside the `IndirectSlowDouble` method prevent the deadlock? http://www.tugberkugurlu.com/archive/the-perfect-recipe-to-shoot-yourself-in-the-foot-ending-up-with-a-deadlock-using-the-c-sharp-5-0-asynchronous-language-features – tugberk Nov 29 '12 at 09:09
  • Adding cross-reference links: http://stackoverflow.com/questions/12981490/task-waitall-hanging-with-multiple-awaitable-tasks-in-asp-net http://stackoverflow.com/questions/11369614/asp-net-synchronizationcontext-locks-httpapplication-for-async-continuations – Marc Gravell Nov 29 '12 at 09:57
  • Actually, the same timeout will happen in WinForms. It doesn't happen in console apps. I wrote [an article on SynchronizationContext](http://msdn.microsoft.com/en-us/magazine/gg598924.aspx) that explains these differences. – Stephen Cleary Nov 29 '12 at 13:32
  • @StephenCleary I stand corrected - there must have been something dodgy in my test that meant it didn't have a sync-context – Marc Gravell Nov 29 '12 at 15:50

3 Answers3

11

It's to do with the way the synchronization context is implemented in ASP.NET (Pre .NET 4.5). There's tons of questions about this behavior:

Task.WaitAll hanging with multiple awaitable tasks in ASP.NET

Asp.net SynchronizationContext locks HttpApplication for async continuations?

In ASP.NET 4.5, there's a new implementation of the sync context that's described in this article.

http://blogs.msdn.com/b/webdev/archive/2012/11/19/all-about-httpruntime-targetframework.aspx

Community
  • 1
  • 1
davidfowl
  • 37,120
  • 7
  • 93
  • 103
  • 1
    That new sync-context looks promising – Marc Gravell Nov 29 '12 at 09:19
  • "ASP.NET 4.5" - it that current ASP.NET running on .NET 4.5? or is that a pending release? I'm targeting 4.5 etc, and have tried explicitly setting all those flags, but it doesn't change the behavior as far as I can tell – Marc Gravell Nov 29 '12 at 09:26
  • 1
    It's on by default if you target .NET 4.5 but it won't make the hang go away. Like the article says, " Importantly, the behavior of async / await is undefined in ASP.NET unless this switch has been set.". You still need to use await everywhere to avoid deadlocks. – davidfowl Nov 29 '12 at 09:32
  • so the naming is a little confusing ... it isn't so much "...TaskFriendly..." as "...TaskSlightlyLessSucky..." ;p Thanks for the clarification. – Marc Gravell Nov 29 '12 at 09:39
  • The pre-4.5 ASP.NET `SynchronizationContext` only allows one thread in at a time. AFAIK, the .NET 4.5 ASP.NET `SynchronizationContext` has the same limitation, so the new version wouldn't fix the timeout. I think the best solution is to make your action `async` and use a `CancellationToken`-based timeout. – Stephen Cleary Nov 29 '12 at 13:34
8

When you use .Result there is always a possibility of deadlock because .Result is blocking by nature. The way to avoid deadlocks is to not block on Tasks (you should use async and await all the way down). The subject is in details described here:

One fix is to add ConfigureAwait:

public static async Task<long> IndirectSlowDouble(long val)
{
    long result = await SlowDouble(val).ConfigureAwait(false);

    return result;
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
tpeczek
  • 23,867
  • 3
  • 74
  • 77
  • blocking is not deadlocking; and the code shown in the question correctly uses a wait-with-timeout. Ultimately, this *is* all capable of completing - see the last line in the question: it is absolutely fine outside of MVC. And even then: it is only the addition of the formally async method that breaks it. But yes, it looks like the ASP.NET sync-context is part of this. – Marc Gravell Nov 29 '12 at 09:07
  • edited to add the `ConfigureAwait`, as that solves the issue in terms of library code, which came directly from one of the linked pages – Marc Gravell Nov 29 '12 at 09:14
  • @MarcGravell Of course blocking is not deadlocking, but blocking behaviour might lead to deadlock. The reason here seems to be the specific of "context" (in case of ASP.NET it is request context) and the same thing will most probably not work in WebForms not only ASP.NET MVC. If you sure that isn't the case I will remove the answer. – tpeczek Nov 29 '12 at 09:15
  • No, it is a good answer - please don't remove it. The sync-context is the main culprit here, it seems. – Marc Gravell Nov 29 '12 at 09:41
6

Another fix is to use async/await throughout:

public async Task<ActionResult> Index()
{
    var task = IndirectSlowDouble(10);
    long result = await task;
    ViewBag.Message = result.ToString();
    return View();
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Yep, this is the option I would prefer but I would still use `ConfigureAwait` at the library level where you don't need the sync context. Especially, when you distribute your library for others to use. – tugberk Nov 29 '12 at 09:41
  • @tugberk returning a `ConfiguredTaskAwaitable[]` is **not** what most callers would expect - it is pretty unclear compared to `Task[]`. I think that is something that would have to be applied by the subscriber. Also: it is not clear, without knowing the caller, whether it *should* wait on the context or not. – Marc Gravell Nov 29 '12 at 09:46
  • But if you `await` an async method inside an async method in ur library, ur consumer consumes that in a blocking fashion and there is an available sync context, u will still end up with a deadlock. OTOH, at the app level, u nearly always wouldn't want to use `ConfigureAwait`. My blog post I linked above tries to explain that based on on Stephan Toub's //build 2011 async talk. – tugberk Nov 29 '12 at 09:57
  • @tugberk oh sure, any `await` code *inside* the library should do that... in my case, though, I'm talking about a library that exposes `Task` / `Task`, but doesn't actually do any `await` internally (or any other kind of voodoo - the tasks are actually `TaskCompletionSource`-based) – Marc Gravell Nov 29 '12 at 10:05
  • Yes, there is no need there unless you capture the sync context manually or use the equivalent TaskScheduler and try to post back to that sync context which would be a bad thing to do on a lib code. Some complains that ConfigureAwait is not on .NET 4.0 but we don't need it if we go pure TPL. – tugberk Nov 29 '12 at 10:12