5

We've been running into a lot of deadlocks as part of exposing some of existing code over Web API. I've been able to distill the problem down to this very simple example that will hang forever:

public class MyController : ApiController
{
    public Task Get()
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

        return Task.FromResult(1)
            .ContinueWith(_ => { }, context)
            .ContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()), context);
    }
}

To me, that code seems simple enough. This might seem a bit contrived but that's only because I've tried simplifying the problem as much as possible. It seems having two ContinueWiths chained like this will cause the deadlock - if I comment out the first ContinueWith (that isn't really doing anything anyway), it will work just fine. I can also 'fix' it by not giving the specific scheduler (but that isn't a workable solution for us since our real code needs to be on the correct/original thread). Here, I've put the two ContinueWiths right next to each other but in our real application there is a lot of logic that is happening and the ContinueWiths end up coming from different methods.

I know I could re-write this particular example using async/await and it would simplify things and seems to fix the deadlock. However, we have a ton of legacy code that has been written over the past few years - and most of it was written before async/await came out so it uses ContinueWith's heavily. Re-writing all that logic isn't something we'd like to do right now if we can avoid it. Code like this has worked fine in all the other scenarios we've run into (Desktop App, Silverlight App, Command Line App, etc.) - it's just Web API that is giving us these problems.

Is there anything that can be done generically that can solve this kind of deadlock? I'm looking for a solution that would hopefully not involve re-writing all ContinueWith's to use async/await.

Update:

The code above is the entire code in my controller. I've tried to make this reproducible with the minimal amount of code. I've even done this in a brand new solution. The full steps that I did:

  1. From Visual Studio 2013 Update 1 on Windows 7 (with .NET Framework 4.5.1), create a new Project using the ASP.NET Web Application Template
  2. Select Web API as the template (on the next screen)
  3. Replace the Get() method in the auto-created ValuesController with the example given in my original code
  4. Hit F5 to start the app and navigate to ./api/values - the request will hang forever
  5. I've tried hosting the web site in IIS as well (instead of using IIS Express)
  6. I also tried updating all the various Nuget packages so I was on the latest of everything

The web.config is untouched from what the template created. Specifically, it has this:

<system.web>
   <compilation debug="true" targetFramework="4.5" />
   <httpRuntime targetFramework="4.5" />
</system.web>
Stephen McDaniel
  • 2,938
  • 2
  • 24
  • 53
  • we need actual code bits to reproduce the problem. – Yuval Itzchakov May 13 '14 at 19:08
  • @YuvalItzchakov The code I posted is literally the entire code that I wrote. I've been doing all this testing in a brand new project using the standard template with no other custom code. I've updated the question to contain the exact steps necessary (hopefully) to reproduce this. – Stephen McDaniel May 13 '14 at 20:17
  • It appears to be a deadlock of the same nature I described here: http://stackoverflow.com/q/23062154/1768303 – noseratio May 13 '14 at 21:04
  • @Noseratio I do seem to have a similar issue to the one mentioned in that question. But in that question, the deadlock seems to be a secondary issue that wasn't really addressed. :-( – Stephen McDaniel May 13 '14 at 22:00
  • @StephenMcDaniel, perhaps, [this](http://stackoverflow.com/a/23642520/1768303) can help, but I haven't tried it myself. – noseratio May 13 '14 at 22:24

3 Answers3

3

Try the following (untested). It's based on the idea that AspNetSynchronizationContext.Send executes the callback synchronously and thus should not result in the same deadlock. This way, we enter the AspNetSynchronizationContext on a random pool thread:

public class MyController : ApiController
{
    public Task Get()
    {
        // should be AspNetSynchronizationContext
        var context = SynchronizationContext.Current;

        return Task.FromResult(1)
            .ContinueWith(_ => { }, TaskScheduler.Default)
            .ContinueWith(_ =>
            {
                object result = null;
                context.Send(__ => { result = Ok(DateTime.Now.ToLongTimeString()); }, 
                    null);
                return result;
            }, TaskScheduler.Default);
    }
}

Updated, based on the comments, apparently it works and eliminates the deadlock. Further, I'd build a custom task scheduler on top of this solution, and use it instead of TaskScheduler.FromCurrentSynchronizationContext(), with very minimal changes to the existing code base.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • If the code will execute synchronously, what is the reason to use Task? – Toan Nguyen May 13 '14 at 22:59
  • @ToanNguyen, which code? The task's lambda passed to `ContinueWith` is executing **asynchronously**, as queued to thread pool by `TaskSheduler.Default`. Inside it, the `context.Send` lambda is executed synchronously, exactly as I want it to be. Its only purpose is to be executed on the correct synchronization context. Had I not put `contex.Send` there, the `Ok()` method would be executed synchronously anyway, but on the wrong context. – noseratio May 13 '14 at 23:08
  • 1
    That code does run without deadlocking (with some minor tweaks to make it compile). But it doesn't seem like a very feasible solution. In my real codebase, the logic inside the ContinueWith's is buried deep in business logic that doesn't know that it is running under Web API so it doesn't know it needs to jump through all these hoops...and I wouldn't really want it to know that anyway. This seems like a very 'local' and invasive change unless I'm missing something. But it doesn't deadlock so that is a good start! – Stephen McDaniel May 13 '14 at 23:17
  • @StephenMcDaniel, I don't think there's a perfect solution here, unless you're willing to convert anything to `async/await` and thus not depend on `TaskSheduler.FromCurrentSynchronizationContext()`. That's because `AspNetSynchronizationContext.Post` is implemented [this way](http://referencesource.microsoft.com/#System.Web/xsp/system/Web/Util/SynchronizationHelper.cs#f0184c54fac66559), which can provoke deadlocks for `ContinueWith`-style tasks. – noseratio May 13 '14 at 23:21
  • 2
    @StephenMcDaniel, from the second thought, you can build a custom task scheduler on top of my solution, and use it instead of `TaskScheduler.FromCurrentSynchronizationContext()`, with very minimal changes to your code base. Derive from `TaskScheduler` and implement `QueueTask` accordingly. There's a bunch of custom task scheduler examples available on SO, grab one for a skeleton. – noseratio May 13 '14 at 23:48
  • @Noseratio Interesting. I just went a similiar way. Instead of a custom scheduler, I went with a custom ContinueWith method. I feel like that might be the least invasive way. I think that might read more naturally throughout my codebase instead of having to use a custom scheduler? But these solutions still seem a bit off. Seems like I'm using a hammer to kill fly kind of thing. – Stephen McDaniel May 13 '14 at 23:54
  • 2
    @StephenMcDaniel, I'd rather prefer a static custom task scheduler, as that would mimic the existing pattern and account for the possible implicit use of `TaskSchduler.Current` by TPL or any other libraries involved in your project. But then again, I'd prefer `async/await` to that. Anyhow, this is as much as I could help here. – noseratio May 14 '14 at 00:03
1

Based on Noseratio's answer, I came up with the following 'Safe' versions of ContinueWith. When I update my code to use these safe versions, I don't have anymore deadlocks. Replacing all my existing ContinueWiths with these SafeContinueWiths probably won't be too bad....it certainly seems easier and safer than re-writing them to use async/await. And when this executes under non-ASP.NET contexts (WPF App, Unit Tests, etc.), it will fall back to the standard ContinueWith behavior so I should have perfect backwards compatability.

I'm still not sure this is the best solution. It seems like this is a pretty heavy-handed approach that is necessary for code that seems so simple.

With that said, I'm presenting this answer in case it triggers a great idea from somebody else. I feel like this can't be the ideal solution.

New controller code:

public Task Get()
{
    return Task.FromResult(1)
               .SafeContinueWith(_ => { })
               .SafeContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()));
}

And then the actual implementation of SafeContinueWith:

public static class TaskExtensions
{
    private static bool IsAspNetContext(this SynchronizationContext context)
    {
        //Maybe not the best way to detect the AspNetSynchronizationContext but it works for now
        return context != null && context.GetType().FullName == "System.Web.AspNetSynchronizationContext";
    }

    /// <summary>
    /// A version of ContinueWith that does some extra gynastics when running under the ASP.NET Synchronization 
    /// Context in order to avoid deadlocks.  The <see cref="continuationFunction"/> will always be run on the 
    /// current SynchronizationContext so:
    /// Before:  task.ContinueWith(t => { ... }, TaskScheduler.FromCurrentSynchronizationContext());
    /// After:   task.SafeContinueWith(t => { ... });
    /// </summary>
    public static Task<T> SafeContinueWith<T>(this Task task, Func<Task,T> continuationFunction)
    {
        //Grab the context
        var context = SynchronizationContext.Current;

        //If we aren't in the ASP.NET world, we can defer to the standard ContinueWith
        if (!context.IsAspNetContext())
        {
            return task.ContinueWith(continuationFunction, TaskScheduler.FromCurrentSynchronizationContext());
        }

        //Otherwise, we need our continuation to be run on a background thread and then synchronously evaluate
        //  the continuation function in the captured context to arive at the resulting value
        return task.ContinueWith(t =>
        {
            var result = default(T);
            context.Send(_ => result = continuationFunction(t), null);
            //TODO: Verify that Send really did complete synchronously?  I think it's required to by Contract?
            //      But I'm not sure I'd want to trust that if I end up using this in producion code.
            return result;
        });
    }

    //Same as above but for non-generic Task input so a bit simpler
    public static Task SafeContinueWith(this Task task, Action<Task> continuation)
    {
        var context = SynchronizationContext.Current;
        if (!context.IsAspNetContext())
        {
            return task.ContinueWith(continuation, TaskScheduler.FromCurrentSynchronizationContext());
        }

        return task.ContinueWith(t => context.Send(_ => continuation(t), null));
    }
}
Community
  • 1
  • 1
Stephen McDaniel
  • 2,938
  • 2
  • 24
  • 53
  • Or you could create wrap the login in a custom synchronization context, and then set it as the current synchronization context. – Toan Nguyen May 14 '14 at 00:35
  • 1
    @ToanNguyen, that might not be a good idea, as some code inside ASP.NET specifically checks if `SynchronizationContext.Current` is `AspNetSynchronizationContext`. – noseratio May 14 '14 at 01:56
  • @Noseratio You will still be able to check it in your wrapping context. – Toan Nguyen May 14 '14 at 01:58
  • 1
    @ToanNguyen, I'm talking about the ASP.NET runtime code, not any custom code. Besides, installing a custom synchronization context like that in an ASP.NET app may affect any `async/await`-based code. This would be a slippery slope. – noseratio May 14 '14 at 02:06
0

You can set TaskContinuationOptions.ExecuteSynchronously:

return Task.FromResult(1)
    .ContinueWith(_ => { }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, context)
    .ContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, context);

There is also a "global" way to get this to work; in your web.config, add this to your appSettings:

<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />

However, I can't really recommend the global approach. With that appsetting, you can't use async/await in your application.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    ExecuteSynchronously doesn't seem to be a safe solution. Isn't that just a 'hint' that might be ignored? Also, could that result in the Continuation running on a background thread - say, if the first task completed on a background thread..? In that case, I couldn't have the Continuation run synchronously because then the continuation would be on the 'wrong' thread and who knows what could go wrong. – Stephen McDaniel May 13 '14 at 21:34
  • And yes, falling back to the legacy Synchronization Context is not an option for us. When we were still back in .NET 4.0, we had all kinds of deadlocks and other issues with that. We have lots of old code that uses ContinueWith but we also have lots of new code that uses async/await. – Stephen McDaniel May 13 '14 at 21:35
  • If the code is changed to use TaskContinuationOptions.ExecuteSynchronously, then why not remove all Task related operations? – Toan Nguyen May 13 '14 at 21:46
  • [`ExecuteSynchronously` can be ignored in some situations](http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx); specifically, if the code is close to a stack overflow, if the request thread is aborted, or if the target scheduler rejects the task. None of those should happen here. The scheduler "overrides" `ExecuteSynchronously`, so there's no danger of running on the wrong thread. – Stephen Cleary May 14 '14 at 03:23
  • 1
    If you want the *best* solution, then convert to `async`/`await`. Anything else is going to be at least somewhat hackish. – Stephen Cleary May 14 '14 at 03:25
  • Converting to async/await is certainly our only valid mid/long-term solution. In the short-term, we might just go with a more 'hackish' solution. It's just a bit sad that ContinueWith's were perfectly fine and seemed to be the best way to use Tasks just a little while ago. Now they are hacks and cause unexplainable deadlocks. Oh well, async/await is certainly a nice ship to be jumping to! – Stephen McDaniel May 15 '14 at 18:28