Beim Aufruf von StackExchange.Redis gerate ich in eine Deadlock-Situation .
Ich weiß nicht genau, was los ist, was sehr frustrierend ist, und ich würde mich über jede Eingabe freuen, die zur Lösung oder Umgehung dieses Problems beitragen könnte.
Falls Sie dieses Problem auch haben und dies alles nicht lesen möchten; Ich schlage vor , dass Sie Einstellung versuchen würden
PreserveAsyncOrder
zufalse
.
ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Dies wird wahrscheinlich die Art von Deadlock beheben, um die es in diesen Fragen und Antworten geht, und könnte auch die Leistung verbessern.
Unser Setup
- Der Code wird entweder als Konsolenanwendung oder als Azure Worker-Rolle ausgeführt.
- Es macht eine REST-API mit HttpMessageHandler verfügbar, sodass der Einstiegspunkt asynchron ist.
- Einige Teile des Codes weisen eine Thread-Affinität auf (gehört einem einzelnen Thread und muss von diesem ausgeführt werden).
- Einige Teile des Codes sind nur asynchron.
- Wir führen die Sync-over-Async- und Async-over-Sync- Anti-Patterns durch. (Mischen
await
undWait()
/Result
). - Wir verwenden nur asynchrone Methoden, wenn wir auf Redis zugreifen.
- Wir verwenden StackExchange.Redis 1.0.450 für .NET 4.5.
Sackgasse
Wenn die Anwendung / der Dienst gestartet wird, läuft sie eine Weile normal, dann funktionieren plötzlich (fast) alle eingehenden Anforderungen nicht mehr und sie erzeugen nie eine Antwort. Alle diese Anforderungen sind blockiert und warten darauf, dass ein Anruf bei Redis abgeschlossen wird.
Interessanterweise bleibt jeder Aufruf von Redis hängen, sobald der Deadlock auftritt, jedoch nur, wenn diese Aufrufe von einer eingehenden API-Anforderung stammen, die im Thread-Pool ausgeführt wird.
Wir rufen Redis auch von Hintergrund-Threads mit niedriger Priorität an, und diese Aufrufe funktionieren auch nach dem Auftreten des Deadlocks weiter.
Es scheint, als würde ein Deadlock nur auftreten, wenn Redis in einem Thread-Pool-Thread aufgerufen wird. Ich denke nicht mehr, dass dies auf die Tatsache zurückzuführen ist, dass diese Aufrufe in einem Thread-Pool-Thread erfolgen. Es scheint eher so, als würde jeder asynchrone Redis-Aufruf ohne Fortsetzung oder mit einer synchronen sicheren Fortsetzung auch nach dem Auftreten der Deadlock-Situation weiter funktionieren. (Siehe, was meiner Meinung nach unten passiert )
verbunden
StackExchange.Redis Deadlocking
Deadlock durch Mischen
await
undTask.Result
(Sync-over-Async, wie wir). Unser Code wird jedoch ohne Synchronisationskontext ausgeführt, sodass dies hier nicht zutrifft, oder?Wie kann man Sync- und Async-Code sicher mischen?
Ja, das sollten wir nicht tun. Aber wir tun es und wir müssen es noch eine Weile tun. Viel Code, der in die asynchrone Welt migriert werden muss.
Auch hier haben wir keinen Synchronisationskontext, daher sollte dies keine Deadlocks verursachen, oder?
Die Einstellung
ConfigureAwait(false)
vor einerawait
hat keine Auswirkung darauf.Timeout-Ausnahme nach asynchronen Befehlen und Task.WhenAny wartet in StackExchange.Redis
Dies ist das Problem der Thread-Entführung. Wie ist die aktuelle Situation dazu? Könnte dies hier das Problem sein?
Der asynchrone Aufruf von StackExchange.Redis hängt
Aus Marc's Antwort:
... mischen Warten und Warten ist keine gute Idee. Zusätzlich zu Deadlocks ist dies "Sync over Async" - ein Anti-Pattern.
Er sagt aber auch:
SE.Redis umgeht den Synchronisationskontext intern (normal für Bibliothekscode), daher sollte es keinen Deadlock geben
Nach meinem Verständnis sollte StackExchange.Redis daher unabhängig davon sein, ob wir das Sync-over-Async- Anti-Pattern verwenden. Es wird einfach nicht empfohlen, da dies die Ursache für Deadlocks in anderem Code sein kann.
In diesem Fall befindet sich der Deadlock jedoch, soweit ich das beurteilen kann, tatsächlich in StackExchange.Redis. Bitte korrigieren Sie mich, falls ich falsch liege.
Debug-Ergebnisse
Ich habe festgestellt , dass die Blockade die Quelle in zu haben scheint ProcessAsyncCompletionQueue
auf Linie 124 vonCompletionManager.cs
.
Ausschnitt aus diesem Code:
while (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
// if we don't win the lock, check whether there is still work; if there is we
// need to retry to prevent a nasty race condition
lock(asyncCompletionQueue)
{
if (asyncCompletionQueue.Count == 0) return; // another thread drained it; can exit
}
Thread.Sleep(1);
}
Ich habe das während des Deadlocks gefunden; activeAsyncWorkerThread
ist einer unserer Threads, der darauf wartet, dass ein Redis-Aufruf abgeschlossen wird. ( unser Thread = ein Thread-Pool-Thread, in dem unser Code ausgeführt wird ). Die obige Schleife wird also für immer fortgesetzt.
Ohne die Details zu kennen, fühlt sich das sicher falsch an. StackExchange.Redis wartet auf einen Thread, von dem es glaubt, dass er der aktive asynchrone Worker-Thread ist, während es sich tatsächlich um einen Thread handelt, der genau das Gegenteil davon ist.
Ich frage mich, ob dies auf das Problem der Thread-Entführung zurückzuführen ist (das ich nicht vollständig verstehe).
Was ist zu tun?
Die beiden wichtigsten Fragen, die ich herauszufinden versuche:
Könnte das Mischen
await
undWait()
/Result
oder die Ursache für Deadlocks sein, selbst wenn es ohne Synchronisationskontext ausgeführt wird?Stoßen wir in StackExchange.Redis auf einen Fehler / eine Einschränkung?
Eine mögliche Lösung?
Aus meinen Debug-Ergebnissen geht hervor, dass das Problem darin besteht, dass:
next.TryComplete(true);
... in Zeile 162 inCompletionManager.cs
könnte unter bestimmten Umständen den aktuellen Thread (der der aktive asynchrone Worker-Thread ist ) abwandern lassen und mit der Verarbeitung von anderem Code beginnen, was möglicherweise zu einem Deadlock führen kann.
Ohne die Details zu kennen und nur über diese "Tatsache" nachzudenken, erscheint es logisch, den aktiven asynchronen Worker-Thread während des Aufrufs vorübergehend freizugeben TryComplete
.
Ich denke, dass so etwas funktionieren könnte:
// release the "active thread lock" while invoking the completion action
Interlocked.CompareExchange(ref activeAsyncWorkerThread, 0, currentThread);
try
{
next.TryComplete(true);
Interlocked.Increment(ref completedAsync);
}
finally
{
// try to re-take the "active thread lock" again
if (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
break; // someone else took over
}
}
Ich denke, meine beste Hoffnung ist, dass Marc Gravell dies liest und Feedback gibt :-)
Kein Synchronisationskontext = Der Standard-Synchronisationskontext
Ich habe oben geschrieben, dass unser Code keinen Synchronisationskontext verwendet . Dies ist nur teilweise richtig: Der Code wird entweder als Konsolenanwendung oder als Azure Worker-Rolle ausgeführt. In diesen Umgebungen SynchronizationContext.Current
ist null
, weshalb ich schrieb , dass wir laufen ohne Synchronisationskontext.
Nach dem Lesen von "Alles dreht sich um den Synchronisierungskontext" habe ich jedoch festgestellt, dass dies nicht wirklich der Fall ist:
Wenn der aktuelle SynchronizationContext eines Threads null ist, hat er implizit einen Standard-SynchronizationContext.
Der Standardsynchronisationskontext sollte jedoch nicht die Ursache für Deadlocks sein, wie dies der UI-basierte Synchronisationskontext (WinForms, WPF) könnte - da dies keine Thread-Affinität impliziert.
Was ich denke passiert
Wenn eine Nachricht abgeschlossen ist, wird ihre Abschlussquelle daraufhin überprüft, ob sie als synchronisationssicher gilt . Wenn dies der Fall ist, wird die Abschlussaktion inline ausgeführt und alles ist in Ordnung.
Ist dies nicht der Fall, besteht die Idee darin, die Abschlussaktion für einen neu zugewiesenen Thread-Pool-Thread auszuführen. Auch das funktioniert gut, wenn es ConnectionMultiplexer.PreserveAsyncOrder
ist false
.
Wenn dies ConnectionMultiplexer.PreserveAsyncOrder
jedoch true
der Standardwert ist, serialisieren diese Thread-Pool-Threads ihre Arbeit mithilfe einer Abschlusswarteschlange und stellen sicher, dass höchstens einer von ihnen zu jedem Zeitpunkt der aktive asynchrone Worker-Thread ist .
Wenn ein Thread zum aktiven asynchronen Worker-Thread wird, bleibt dies so lange bestehen, bis die Abschlusswarteschlange leer ist .
Das Problem ist, dass die Abschlussaktion nicht synchronisierungssicher ist (von oben), sie jedoch in einem Thread ausgeführt wird, der nicht blockiert werden darf, da dadurch verhindert wird, dass andere nicht synchronisierungssichere Nachrichten abgeschlossen werden.
Beachten Sie, dass andere Nachrichten, die mit einer synchronisierungssicheren Abschlussaktion abgeschlossen werden, weiterhin einwandfrei funktionieren, obwohl der aktive asynchrone Worker-Thread blockiert ist.
Mein vorgeschlagener "Fix" (oben) würde auf diese Weise keinen Deadlock verursachen, würde jedoch den Gedanken , die asynchrone Abschlussreihenfolge beizubehalten, durcheinander bringen .
Also vielleicht der Abschluss hier zu machen ist , dass es nicht sicher ist , zu mischen await
mit Result
/ Wait()
wenn PreserveAsyncOrder
isttrue
, ganz gleich , ob wir ohne Synchronisationskontext laufen?
( Zumindest bis wir .NET 4.6 und das neue verwenden können TaskCreationOptions.RunContinuationsAsynchronously
, nehme ich an )