wake-up-neo.net

Warum hängt diese asynchrone Aktion?

Ich habe eine mehrschichtige .Net 4.5-Anwendung, die eine Methode mit den neuen Schlüsselwörtern async und await von C # aufruft, die nur hängt und ich kann nicht verstehen, warum.

Unten habe ich eine asynchrone Methode, die unser Datenbank-Dienstprogramm OurDBConn erweitert (im Grunde genommen ein Wrapper für die zugrunde liegenden Objekte DBConnection und DBCommand):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Dann habe ich eine mittlere asynchrone Methode, die dies aufruft, um einige langsam laufende Summen zu erhalten:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Endlich habe ich eine UI-Methode (eine MVC-Aktion), die synchron läuft:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Das Problem ist, dass es für immer an dieser letzten Zeile hängt. Das gleiche passiert, wenn ich asyncTask.Wait() aufrufe. Wenn ich die langsame SQL-Methode direkt ausführe, dauert es ungefähr 4 Sekunden.

Das Verhalten, das ich erwarte, ist, dass, wenn es zu asyncTask.Result Kommt, es warten sollte, bis es fertig ist, und wenn es fertig ist, sollte es das Ergebnis zurückgeben.

Wenn ich mit einem Debugger durchsteige, wird die SQL-Anweisung abgeschlossen und die Lambda-Funktion beendet, aber die Zeile return result; Von GetTotalAsync wird nie erreicht.

Irgendeine Idee, was ich falsch mache?

Irgendwelche Vorschläge, wo ich nachforschen muss, um dies zu beheben?

Könnte dies eine Sackgasse sein, und wenn ja, gibt es einen direkten Weg, um es zu finden?

95
Keith

Ja, das ist eine Sackgasse. Und ein häufiger Fehler mit der TPL, also fühlen Sie sich nicht schlecht.

Wenn Sie await foo Schreiben, plant die Laufzeit standardmäßig die Fortsetzung der Funktion in demselben SynchronizationContext, in dem die Methode gestartet wurde. Nehmen wir auf Englisch an, Sie haben Ihr ExecuteAsync über den UI-Thread aufgerufen. Ihre Abfrage wird im Threadpool-Thread ausgeführt (weil Sie Task.Run Aufgerufen haben), aber Sie warten dann auf das Ergebnis. Dies bedeutet, dass die Laufzeit die Ausführung Ihrer Zeile "return result;" Für den UI-Thread einplant, anstatt sie für den Threadpool zurückzuplanen.

Wie funktioniert diese Sackgasse? Stellen Sie sich vor, Sie haben nur diesen Code:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

Die erste Zeile startet also die asynchrone Arbeit. Die zweite Zeile dann blockiert den UI-Thread. Wenn die Laufzeit die Zeile "return result" im UI-Thread ausführen möchte, ist dies erst nach Abschluss von Result möglich. Aber natürlich kann das Ergebnis erst dann angegeben werden, wenn die Rücksendung erfolgt. Sackgasse.

Dies zeigt eine wichtige Regel für die Verwendung der TPL: Wenn Sie .Result In einem UI-Thread (oder einem anderen ausgefallenen Synchronisierungskontext) verwenden, müssen Sie darauf achten, dass für die Benutzeroberfläche keine Zeitpläne für Aufgaben erstellt werden Faden. Sonst passiert Böses.

Also, was machst du? Option # 1 wird überall verwendet, aber wie Sie sagten, ist dies bereits keine Option. Die zweite Möglichkeit, die Ihnen zur Verfügung steht, besteht darin, die Verwendung von wait einfach zu beenden. Sie können Ihre beiden Funktionen folgendermaßen umschreiben:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Was ist der Unterschied? Es gibt jetzt kein Warten mehr, daher wird nichts implizit für den UI-Thread geplant. Für einfache Methoden wie diese, die eine einzige Rückgabe haben, ist es sinnlos, ein "var result = await...; return result" - Muster zu erstellen. Entfernen Sie einfach den Async-Modifikator und übergeben Sie das Task-Objekt direkt. Es ist weniger Overhead, wenn nichts anderes.

Option # 3 gibt an, dass Sie nicht möchten, dass Ihre Wartezeiten zum UI-Thread zurückkehren, sondern nur zum UI-Thread. Sie tun dies mit der ConfigureAwait -Methode wie folgt:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

Wenn Sie normalerweise auf eine Aufgabe warten, wird dies für den UI-Thread geplant, wenn Sie sich in diesem befinden. Wenn Sie auf das Ergebnis von ContinueAwait warten, wird der aktuelle Kontext ignoriert und es wird immer ein Zeitplan für den Threadpool erstellt. Der Nachteil dabei ist, dass Sie dies streuen müssen überall in allen Funktionen, von denen Ihr .Result abhängt, da ein verpasster .ConfigureAwait Die Ursache für einen weiteren Deadlock sein kann.

139

Dies ist das klassische gemischte Deadlock-Szenario -asyncwie ich in meinem Blog beschreibe . Jason hat es gut beschrieben: Standardmäßig wird bei jedem await ein "Kontext" gespeichert und verwendet, um die async -Methode fortzusetzen. Dieser "Kontext" ist der aktuelle SynchronizationContext, es sei denn, es ist null. In diesem Fall ist es der aktuelle TaskScheduler. Wenn die Methode async fortzufahren versucht, wird der erfasste "Kontext" (in diesem Fall ein ASP.NET SynchronizationContext) erneut eingegeben. In ASP.NET SynchronizationContext ist jeweils nur ein Thread im Kontext zulässig, und es ist bereits ein Thread im Kontext vorhanden - der Thread ist auf Task.Result Blockiert.

Es gibt zwei Richtlinien, die diesen Deadlock vermeiden:

  1. Verwenden Sie async ganz nach unten. Sie erwähnen, dass Sie dies "nicht können", aber ich bin nicht sicher, warum nicht. ASP.NET MVC unter .NET 4.5 kann mit Sicherheit async Aktionen unterstützen, und es ist keine schwierige Änderung.
  2. Verwenden Sie ConfigureAwait(continueOnCapturedContext: false) so oft wie möglich. Dies setzt das Standardverhalten der Wiederaufnahme des erfassten Kontexts außer Kraft.
34
Stephen Cleary

Ich befand mich in der gleichen Deadlock-Situation, aber in meinem Fall, in dem ich eine asynchrone Methode von einer Synchronisationsmethode aus aufrief, funktionierte Folgendes für mich:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

ist das ein guter Ansatz, irgendeine Idee?

11
Danilow