27

Below is a simplified version of the code I'm having trouble with. When I run this in a console application, it works as expected. All queries are run in parallel and Task.WaitAll() returns when they are all complete.

However, when this code runs in a web application, the request just hangs. When I attach a debugger and break all, it shows that execution is wait on Task.WaitAll(). And the first task has completed, but the others never finish.

I can't figure out why it hangs when running in ASP.NET, but works fine in a console application.

public Foo[] DoWork(int[] values)
{
    int count = values.Length;
    Task[] tasks = new Task[count];

    for (int i = 0; i < count; i++)
    {
        tasks[i] = GetFooAsync(values[i]);
    }

    try
    {
        Task.WaitAll(tasks);
    }
    catch (AggregateException)
    {
        // Handle exceptions
    }

    return ...
}

public async Task<Foo> GetFooAsync(int value)
{
    Foo foo = null;

    Func<Foo, Task> executeCommand = async (command) =>
    {
        foo = new Foo();

        using (SqlDataReader reader = await command.ExecuteReaderAsync())
        {
            ReadFoo(reader, foo);
        }
    };

    await QueryAsync(executeCommand, value);

    return foo;
}

public async Task QueryAsync(Func<SqlCommand, Task> executeCommand, int value)
{
    using (SqlConnection connection = new SqlConnection(...))
    {
        connection.Open();

        using (SqlCommand command = connection.CreateCommand())
        {
            // Set up query...

            await executeCommand(command);

            // Log results...

            return;
        }
    }           
}
Servy
  • 202,030
  • 26
  • 332
  • 449
John Rutherford
  • 10,704
  • 7
  • 30
  • 32

1 Answers1

53

Rather than Task.WaitAll you need to use await Task.WhenAll.

In ASP.NET you have an actual synchronization context. This means that after all await calls you will be marshaled back to that context to execute the continuation (effectively serializing these continuations). In a console app there is no synchronization context, so all of the continuations are just sent to the thread pool. By using Task.WaitAll in the request's context you're blocking it, which is preventing it from being used to handle the continuations from all of the other tasks.

Also note that one of the primary benefits of async/await in an ASP app is to not block the thread pool thread that you're using to handle the request. If you use a Task.WaitAll you're defeating that purpose.

A side effect of making this change is that by moving from a blocking operation to an await operation exceptions will be propagated differently. Rather than throwing AggregateException it will throw one of the underlying exceptions.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • 1
    +1. Though I would use the term "request context" instead of "main thread". – Stephen Cleary Oct 19 '12 at 20:20
  • @StephenCleary Yeah, it was debatable. I put main thread in quotes because it's not the entire application's main thread, but it can be thought of as "that request's main thread". It's a useful way of thinking of the problem in my mind. – Servy Oct 19 '12 at 20:21
  • 4
    One important caveat is that the await Task.WhenAll will not throw the AggregateException; only one of the inner exceptions will be propagated. If you want to examine the entire AggregateException, you'll need to store a reference to the returned task from task.WhenAll and examine its Exception property explicitly. – Dan Bryant Oct 19 '12 at 20:22
  • @DanBryant Yeah, I wasn't sure if that was worth including or not; to be on the safe side I added a note to that effect. – Servy Oct 19 '12 at 20:24
  • So is it possible to create a non-awaitable version of this method? I want to be able to return Foo[], not Task. – John Rutherford Oct 19 '12 at 20:29
  • 1
    @sectrean If that's the case then you might as well just remove all of the async/await code entirely and just do everything serially using blocking methods. – Servy Oct 19 '12 at 20:34
  • I would still like the queries to run in parallel. I'm refactoring this method which is already being called in many places. Many of the methods it's called from don't return void, so I would have to make them also return Task... – John Rutherford Oct 19 '12 at 20:56
  • 1
    @sectrean Yes, that's essentially the effect of using async/await, everything that either fetches, or uses, the results of an async operation are tasks, unless you do a more profound refactor from the top down. – Servy Oct 19 '12 at 21:01
  • 1
    @Servy After `await` in ASP.NET, you return to the same context, but it could be on a different thread. So I think you can't really call it “that request's main thread”, because which thread is that can change over the lifetime of the request. – svick Oct 19 '12 at 22:00
  • This also applies to Silverlight. – Ryan.Bartsch Aug 06 '13 at 02:54