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 )
quelle
Antworten:
Dies sind die Problemumgehungen, die ich für dieses Deadlock-Problem gefunden habe:
Problemumgehung Nr. 1
Standardmäßig stellt StackExchange.Redis sicher, dass Befehle in derselben Reihenfolge ausgeführt werden, in der Ergebnisnachrichten empfangen werden. Dies kann zu einem Deadlock führen, wie in dieser Frage beschrieben.
Deaktivieren Sie dieses Verhalten durch das Setzen
PreserveAsyncOrder
auffalse
.ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Dies vermeidet Deadlocks und kann auch die Leistung verbessern .
Ich ermutige jeden, der auf Deadlock-Probleme stößt, diese Problemumgehung auszuprobieren, da sie so sauber und einfach ist.
Sie verlieren die Garantie, dass asynchrone Fortsetzungen in derselben Reihenfolge aufgerufen werden, in der die zugrunde liegenden Redis-Vorgänge abgeschlossen sind. Ich verstehe jedoch nicht wirklich, warum Sie sich darauf verlassen würden.
Problemumgehung Nr. 2
Der Deadlock tritt auf, wenn der aktive asynchrone Worker-Thread in StackExchange.Redis einen Befehl abschließt und wenn die Abschlussaufgabe inline ausgeführt wird.
Sie können verhindern, dass eine Aufgabe inline ausgeführt wird, indem Sie eine benutzerdefinierte Aufgabe verwenden
TaskScheduler
und sicherstellen, dass dieseTryExecuteTaskInline
zurückgegeben wirdfalse
.public class MyScheduler : TaskScheduler { public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; // Never allow inlining. } // TODO: Rest of TaskScheduler implementation goes here... }
Das Implementieren eines guten Aufgabenplaners kann eine komplexe Aufgabe sein. Es gibt jedoch bereits Implementierungen in der ParallelExtensionExtras-Bibliothek ( NuGet-Paket ), die Sie verwenden oder von denen Sie sich inspirieren lassen können.
Wenn Ihr Taskplaner eigene Threads verwenden würde (nicht aus dem Thread-Pool), ist es möglicherweise eine gute Idee, Inlining zuzulassen, es sei denn, der aktuelle Thread stammt aus dem Thread-Pool. Dies funktioniert, da der aktive asynchrone Worker-Thread in StackExchange.Redis immer ein Thread-Pool-Thread ist.
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Don't allow inlining on a thread pool thread. return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task); }
Eine andere Idee wäre, Ihren Scheduler mithilfe des thread-lokalen Speichers an alle seine Threads anzuhängen .
private static ThreadLocal<TaskScheduler> __attachedScheduler = new ThreadLocal<TaskScheduler>();
Stellen Sie sicher, dass dieses Feld zugewiesen wird, wenn der Thread ausgeführt und gelöscht wird, wenn er abgeschlossen ist:
private void ThreadProc() { // Attach scheduler to thread __attachedScheduler.Value = this; try { // TODO: Actual thread proc goes here... } finally { // Detach scheduler from thread __attachedScheduler.Value = null; } }
Dann können Sie das Inlining von Aufgaben zulassen, solange diese in einem Thread ausgeführt werden, der dem benutzerdefinierten Scheduler "gehört":
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Allow inlining on our own threads. return __attachedScheduler.Value == this && this.TryExecuteTask(task); }
quelle
PreserveAsyncOrder
ist veraltet.ConnectionMultiplexer
nachdem siePreserveAsyncOrder
veraltet ist? Oder wenn irgendwo anders eine Flagge istStackExchange.Redis
?Ich vermute viel basierend auf den obigen detaillierten Informationen und weiß nicht, welchen Quellcode Sie haben. Es hört sich so an, als ob Sie in .Net einige interne und konfigurierbare Grenzwerte erreichen. Sie sollten diese nicht treffen, daher vermute ich, dass Sie keine Objekte entsorgen, da diese zwischen Threads schweben, sodass Sie keine using-Anweisung verwenden können, um ihre Objektlebensdauer sauber zu handhaben.
Hier werden die Einschränkungen für HTTP-Anforderungen aufgeführt. Ähnlich wie beim alten WCF-Problem, wenn Sie die Verbindung nicht entsorgt haben und dann alle WCF-Verbindungen fehlschlagen würden.
Maximale Anzahl gleichzeitiger HttpWebRequests
Dies ist eher eine Debugging-Hilfe, da ich bezweifle, dass Sie wirklich alle TCP-Ports verwenden, aber gute Informationen darüber, wie Sie feststellen können, wie viele offene Ports Sie haben und wohin.
https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx
quelle