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?

Lawrence Wagerfield
  • 6,471
  • 5
  • 42
  • 84

1 Answers1

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.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 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)? – Lawrence Wagerfield Oct 18 '11 at 08:55
  • @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: http://msdn.microsoft.com/en-us/magazine/hh456402.aspx – Jon Skeet Oct 18 '11 at 08:57
  • 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? – Lawrence Wagerfield Oct 18 '11 at 09:13
  • @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. – Jon Skeet Oct 18 '11 at 09:16
  • 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). – Lawrence Wagerfield Oct 18 '11 at 09:27
  • @LawrenceWagerfield: Agreed, it won't always be appropriate - but I don't know enough about ASP.NET to give more information, unfortunately :( – Jon Skeet Oct 18 '11 at 09:31
  • 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. – Ankur Oct 18 '11 at 11:56
  • @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. – Jon Skeet Oct 18 '11 at 11:57
  • @JonSkeet: Hmm.. is TaskEx.Run same as Task.Factory.StartNew? – Ankur Oct 18 '11 at 12:01
  • @Ankur: Pretty much, yes. It's just a short form. It'll be available as `Task.Run` in .NET 4.5, I think. – Jon Skeet Oct 18 '11 at 12:04
  • `AspNetSynchronizationContext` is the strangest implementation. It [treats `Post` as synchronous rather than asynchronous](http://nitoprograms.blogspot.com/2009/08/gotchas-from-synchronizationcontext.html) and uses a lock to [execute its delegates one at a time](http://msdn.microsoft.com/en-us/magazine/gg598924.aspx). `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). – Stephen Cleary Oct 18 '11 at 13:49
  • @StephenCleary: Ah, thanks - that all makes a bit more sense, in a weird and wonderful way :) Will edit this into the answer. – Jon Skeet Oct 18 '11 at 13:52