Async CTP Bug - Task Never Completes
Asked Answered
M

1

4

Firstly a forward apology: I cannot isolate the following bug into a simple console application. However, in my relatively simple ASP.NET Web Forms application, the following code will cause the current thread to block indefinitely:

public class MyModule : IHttpModule
{
    public void Dispose()
    {
    }

    public void Init(System.Web.HttpApplication context)
    {
        context.BeginRequest += this.Context_BeginRequest;
    }

    private void Context_BeginRequest(object sender, EventArgs e)
    {
        Sleep().Wait();
        var x = 2; // This line is never hit.
    }

    private async Task Sleep()
    {
        await TaskEx.Run(() => System.Threading.Thread.Sleep(1000));
    }
}

The task state just remains as 'WaitingForActivation'. Does anyone know why this would happen?

Misjoinder answered 18/10, 2011 at 8:25 Comment(1)
It works fine in a console exe; it seems related to asp.netPedicular
B
5

EDIT: The comment from Stephen Cleary sheds more light:

AspNetSynchronizationContext is the strangest implementation. It treats Post as synchronous rather than asynchronous and uses a lock to execute its delegates one at a time. AspNetSynchronizationContext does not require marshaling back to the same thread (but does need to take the lock); the deadlock on Wait is because the continuation is waiting for the lock (held by the thread in the event handler)


My guess is that there's a SynchronizationContext which is forcing the continuation to run on the same thread as the event handler. Your event handler is blocking that thread, so the continuation never runs, which means the event handler will never unblock.

That's only a guess though - it's the only thing I can think of which makes sense at the moment.

One option to try to unblock this is to change your Sleep method to:

private async Task Sleep()
{
    await TaskEx.Run(() => System.Threading.Thread.Sleep(1000))
                .ConfigureAwait(continueOnCapturedContext: false);
}

That will allow the continuation to complete on a different context.

I'm surprised there is such a synchronization context, mind you... I'd expect all of this to just happen on the thread pool. Possibly BeginRequest is treated slightly specially.

Broker answered 18/10, 2011 at 8:34 Comment(12)
Thanks for the great insight. Adding the above 'ConfigureAwait' to the method solves the example problem. However, the real async method is actually part of a lower-level assembly, which is also used by other components. Would you recommend I modify all my library TaskEx.Runs in this way, or is there a better way to cure misbehaving calling code (like the IHttpModule in this case)?Misjoinder
@LawrenceWagerfield: It's not clear what's really misbehaving here, to be honest - each bit makes sense in its own right, I suspect. Obviously in real code you don't just want to sleep... if you can give more of an indication of what your real code is trying to do, I may be able to help more. In terms of whether or not to call ConfigureAwait normally, it's probably worth reading Stephen Toub's recent post here: msdn.microsoft.com/en-us/magazine/hh456402.aspxBroker
The underlying library code in this case is fairly simple; it just raises an event which is handled by 2 actions which interact with a database. The problem is definitely isolated to the IHttpModule; the code works fine when called from other areas. Is there a way to override the synchronization context used by the IHttpModule?Misjoinder
@LawrenceWagerfield: Not that I'm aware of - but do you need everything to finish before you continue? Can you just await Sleep() instead of calling Sleep.Wait()? (Make the event handler async as well, in other words.) The downside of that is that the rest of your handler may execute after other stuff has started happening.Broker
Thanks Jon - I think for now the answer is to not use Wait() in IHttpModules, and to make all methods async instead. I can't see this always being viable, as sometimes you will need to interact with an async method from a sync method (consider a URL rewriting module which infers a target URL from an async library method - a Wait will be unavoidable here, as rewriting needs to occur before the BeginRequest event completes).Misjoinder
@LawrenceWagerfield: Agreed, it won't always be appropriate - but I don't know enough about ASP.NET to give more information, unfortunately :(Broker
I would suggest try to use Reflector and see what the C# compiler has generated, it seems to be a sort of bug in the generated code.Strongbox
@Ankur: No, I don't think so. The generated code will just be using Task.ContinueWith. It's what thread the continuation tries to use that will be the issue, I believe.Broker
@JonSkeet: Hmm.. is TaskEx.Run same as Task.Factory.StartNew?Strongbox
@Ankur: Pretty much, yes. It's just a short form. It'll be available as Task.Run in .NET 4.5, I think.Broker
AspNetSynchronizationContext is the strangest implementation. It treats Post as synchronous rather than asynchronous and uses a lock to execute its delegates one at a time. AspNetSynchronizationContext does not require marshaling back to the same thread (but does need to take the lock); the deadlock on Wait is because the continuation is waiting for the lock (held by the thread in the event handler).Gin
@StephenCleary: Ah, thanks - that all makes a bit more sense, in a weird and wonderful way :) Will edit this into the answer.Broker

© 2022 - 2024 — McMap. All rights reserved.