2012-10-19 5 views
23

Unten ist eine vereinfachte Version des Codes, mit dem ich Probleme habe. Wenn ich dies in einer Konsolenanwendung ausführe, funktioniert es wie erwartet. Alle Abfragen werden parallel ausgeführt und Task.WaitAll() wird zurückgegeben, wenn sie alle abgeschlossen sind.Task.WaitAll hängt mit mehreren erwarteten Aufgaben in ASP.NET

Wenn dieser Code jedoch in einer Webanwendung ausgeführt wird, hängt die Anforderung einfach. Wenn ich einen Debugger anschließe und alles abbringe, wird angezeigt, dass die Ausführung auf Task.WaitAll() wartet. Und die erste Aufgabe ist abgeschlossen, aber die anderen enden nie.

Ich kann nicht herausfinden, warum es beim Ausführen in ASP.NET hängt, aber in einer Konsolenanwendung funktioniert.

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; 
     } 
    }   
} 

Antwort

46

Anstatt Task.WaitAll Sie brauchen await Task.WhenAll zu verwenden.

In ASP.NET haben Sie einen tatsächlichen Synchronisationskontext. Dies bedeutet, dass nach allen await Aufrufen Sie in diesen Kontext zurückgemeldet werden, um die Fortsetzung auszuführen (diese Fortsetzungen effektiv serialisieren). In einer Konsolenanwendung gibt es keinen Synchronisationskontext, daher werden alle Fortsetzungen nur an den Thread-Pool gesendet. Wenn Sie im Kontext der Anfrage Task.WaitAll verwenden, blockieren Sie sie, wodurch verhindert wird, dass sie für die Fortsetzungen von allen anderen Aufgaben verwendet wird.

Beachten Sie außerdem, dass einer der Hauptvorteile von async/in einer ASP-Anwendung erwartet ist nicht Block den Thread-Pool-Thread, den Sie verwenden, um die Anforderung zu behandeln. Wenn Sie eine Task.WaitAll verwenden, vereiteln Sie diesen Zweck.

Ein Nebeneffekt dieser Änderung besteht darin, dass beim Übergang von einer blockierenden Operation zu einer abwartenden Operation Ausnahmen anders propagiert werden. Anstatt AggregateException zu werfen, wird eine der zugrunde liegenden Ausnahmen ausgelöst.

+1

+1. Obwohl ich den Begriff "Anfragekontext" anstelle von "Hauptthread" verwenden würde. –

+0

@ StephenCleary Ja, es war umstritten. Ich setze Hauptthread in Anführungszeichen, weil es nicht der Hauptthread der gesamten Anwendung ist, aber man kann es sich als "Hauptthread dieser Anfrage" vorstellen. Es ist eine nützliche Art, das Problem in meinem Kopf zu denken. – Servy

+4

Ein wichtiger Vorbehalt ist, dass die aware Task.WhenAll die AggregateException nicht auslöst; Nur eine der inneren Ausnahmen wird weitergegeben. Wenn Sie die gesamte AggregateException untersuchen möchten, müssen Sie einen Verweis auf die zurückgegebene Task von task.WhenAll speichern und die Exception-Eigenschaft explizit überprüfen. –