Ich arbeite mit einer neuen Codebasis, die stark von async / await Gebrauch macht. Die meisten Leute in meinem Team sind auch noch ziemlich neu im Bereich Async / Warten. Wir halten uns in der Regel an die von Microsoft festgelegten Best Practices , benötigen jedoch im Allgemeinen unseren Kontext, um den asynchronen Aufruf zu verarbeiten, und arbeiten mit Bibliotheken, die dies nicht tun ConfigureAwait(false)
.
Wenn Sie all diese Dinge kombinieren, stoßen wir auf asynchrone Deadlocks, die im Artikel beschrieben werden ... wöchentlich. Sie werden beim Unit-Test nicht angezeigt, da unsere verspotteten Datenquellen (normalerweise über Task.FromResult
) nicht ausreichen, um den Deadlock auszulösen. Während der Laufzeit- oder Integrationstests geht ein Service-Aufruf nur zum Mittagessen aus und kehrt nie zurück. Das bringt die Server zum Erliegen und bringt die Dinge im Allgemeinen durcheinander.
Das Problem besteht darin, dass das Auffinden, wo der Fehler gemacht wurde (normalerweise nicht vollständig asynchron), im Allgemeinen eine manuelle Codeüberprüfung umfasst, die zeitaufwendig und nicht automatisierbar ist.
Wie lässt sich besser diagnostizieren, was den Deadlock verursacht hat?
async
gelesen ?Antworten:
Ok - Ich bin mir nicht sicher, ob das Folgende für Sie hilfreich sein wird, da ich bei der Entwicklung einer Lösung einige Annahmen getroffen habe, die in Ihrem Fall möglicherweise zutreffen oder nicht. Vielleicht ist meine "Lösung" zu theoretisch und funktioniert nur für künstliche Beispiele - ich habe keine Tests durchgeführt, die über die folgenden Punkte hinausgehen.
Darüber hinaus würde ich das Folgende eher als Problemumgehung als als echte Lösung ansehen, aber angesichts des Mangels an Antworten denke ich, dass es immer noch besser als nichts ist (Ich habe Ihre Frage beobachtet und auf eine Lösung gewartet, aber nicht gesehen, dass eine veröffentlicht wurde. Ich habe angefangen zu spielen herum mit der Ausgabe).
Aber genug gesagt: Nehmen wir an, wir haben einen einfachen Datendienst, mit dem eine Ganzzahl abgerufen werden kann:
Eine einfache Implementierung verwendet asynchronen Code:
Jetzt tritt ein Problem auf, wenn wir den Code "falsch" verwenden, wie in dieser Klasse dargestellt.
Foo
greiftTask.Result
nichtawait
wieBar
folgt auf das Ergebnis zu :Was wir (Sie) jetzt brauchen, ist eine Möglichkeit, einen Test zu schreiben, der beim Anrufen erfolgreich ist, beim Anrufen
Bar
jedoch fehlschlägtFoo
(zumindest, wenn ich die Frage richtig verstanden habe ;-)).Ich werde den Code sprechen lassen; Folgendes habe ich mir ausgedacht (mit Visual Studio-Tests, aber es sollte auch mit NUnit funktionieren):
DataServiceMock
nutztTaskCompletionSource<T>
. Auf diese Weise können wir das Ergebnis an einem definierten Punkt im Testlauf einstellen, der zum folgenden Test führt. Beachten Sie, dass wir einen Delegaten verwenden, um die TaskCompletionSource an den Test zurückzugeben. Sie können dies auch in die Initialize-Methode des Tests einfügen und Eigenschaften verwenden.Was hier passiert, ist, dass wir zuerst überprüfen, ob wir die Methode ohne Blockierung verlassen können (dies würde nicht funktionieren, wenn jemand darauf zugreift
Task.Result
- in diesem Fall würden wir in eine Zeitüberschreitung geraten, da das Ergebnis der Aufgabe erst verfügbar gemacht wird, nachdem die Methode zurückgekehrt ist ).Dann setzten wir das Ergebnis (jetzt kann das Verfahren durchführt) und wir das Ergebnis (in einem Gerät zu testen wir Task.Result zugreifen können , wie wir tatsächlich überprüfen wollen die Blockierung auftreten).
Vollständige Testklasse -
BarTest
erfolgreich undFooTest
nicht wie gewünscht.Und eine kleine Helferklasse zum Testen auf Deadlocks / Timeouts:
quelle
Hier ist eine Strategie, die ich in einer riesigen und sehr, sehr vielschichtigen Anwendung angewendet habe:
Zunächst benötigen Sie eine Datenstruktur um einen Mutex (leider) und führen keine Synchronisierung des Anrufverzeichnisses durch. In dieser Datenstruktur gibt es eine Verknüpfung zu einem zuvor gesperrten Mutex. Jeder Mutex hat eine "Ebene" beginnend mit 0, die Sie beim Erstellen des Mutex zuweisen und die sich niemals ändern kann.
Und die Regel lautet: Wenn ein Mutex gesperrt ist, dürfen Sie andere Mutexe nur auf einer niedrigeren Ebene sperren. Wenn Sie diese Regel befolgen, können Sie keine Deadlocks haben. Wenn Sie einen Verstoß feststellen, ist Ihre Anwendung weiterhin funktionsfähig.
Wenn Sie einen Verstoß feststellen, gibt es zwei Möglichkeiten: Möglicherweise haben Sie die Ebenen falsch zugewiesen. Sie haben A und anschließend B gesperrt, sodass B eine niedrigere Stufe haben sollte. Also korrigieren Sie den Pegel und versuchen es erneut.
Die andere Möglichkeit: Sie können es nicht beheben. Ein Code von Ihnen sperrt A, gefolgt von Sperren von B, während ein anderer Code B, gefolgt von Sperren von A, sperrt. Es gibt keine Möglichkeit, die Ebenen zuzuweisen, um dies zuzulassen. Und dies ist natürlich ein möglicher Deadlock: Wenn beide Codes gleichzeitig auf verschiedenen Threads ausgeführt werden, besteht die Möglichkeit eines Deadlocks.
Nach dieser Einführung gab es eine relativ kurze Phase, in der die Pegel angepasst werden mussten, gefolgt von einer längeren Phase, in der potenzielle Deadlocks festgestellt wurden.
quelle
Verwenden Sie Async / Await, um teure Anrufe wie in eine Datenbank zu parallelisieren? Abhängig vom Ausführungspfad in der DB ist dies möglicherweise nicht möglich.
Die Testabdeckung mit Async / Warten kann eine Herausforderung sein, und es gibt nichts Besseres als eine echte Produktionsauslastung, um Fehler zu finden. Ein Muster, das Sie in Betracht ziehen könnten, besteht darin, eine Korrelations-ID zu übergeben und im Stapel zu protokollieren. Anschließend gibt es ein kaskadierendes Zeitlimit, das den Fehler protokolliert. Dies ist eher ein SOA-Muster, aber es gibt Ihnen zumindest einen Eindruck davon, woher es kommt. Wir haben dies mit Splunk verwendet, um Deadlocks zu finden.
quelle