Je suis dans une situation de blocage lors de l'appel StackExchange.Redis .
Je ne sais pas exactement ce qui se passe, ce qui est très frustrant, et j'apprécierais toute contribution qui pourrait aider à résoudre ou à contourner ce problème.
Au cas où vous auriez aussi ce problème et que vous ne voudriez pas lire tout cela; je vous suggère d'essayer de régler
PreserveAsyncOrder
surfalse
.ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Cela résoudra probablement le type de blocage sur lequel porte cette Q&R et pourrait également améliorer les performances.
await
et Wait()
/Result
).Lorsque l'application/le service est démarré, il s'exécute normalement pendant un certain temps, puis tout à coup (presque) toutes les demandes entrantes cessent de fonctionner, elles ne produisent jamais de réponse. Toutes ces demandes sont bloquées en attendant la fin d'un appel à Redis.
Fait intéressant, une fois que le blocage se produit, tout appel à Redis se bloque, mais uniquement si ces appels sont effectués à partir d'une demande d'API entrante, qui est exécutée sur le pool de threads.
Nous effectuons également des appels à Redis à partir de threads d'arrière-plan de faible priorité, et ces appels continuent de fonctionner même après le blocage.
Il semble qu'un blocage se produira uniquement lors de l'appel à Redis sur un thread de pool de threads. Je ne pense plus que cela soit dû au fait que ces appels sont effectués sur un thread de pool de threads. Il semble plutôt que tout appel Redis asynchrone sans suite, ou avec une suite sync safe, continuera à fonctionner même après le blocage. (Voir Ce que je pense se passe ci-dessous)
StackExchange.Redis Deadlocking
Blocage provoqué par le mélange de await
et Task.Result
(Synchronisation sur async, comme nous le faisons). Mais notre code est exécuté sans contexte de synchronisation, ce qui ne s'applique pas ici, non?
Comment mélanger en toute sécurité le code de synchronisation et asynchrone?
Oui, nous ne devrions pas faire ça. Mais nous le faisons, et nous devrons continuer à le faire pendant un certain temps. Beaucoup de code qui doit être migré dans le monde asynchrone.
Encore une fois, nous n'avons pas de contexte de synchronisation, donc cela ne devrait pas provoquer de blocages, non?
Définir ConfigureAwait(false)
avant tout await
n'a aucun effet sur cela.
Il s'agit du problème de détournement de thread. Quelle est la situation actuelle à ce sujet? Serait-ce le problème ici?
l'appel asynchrone StackExchange.Redis se bloque
D'après la réponse de Marc:
... mélanger Attendre et attendre n'est pas une bonne idée. En plus des blocages, il s'agit de "synchronisation sur async" - un anti-modèle.
Mais il dit aussi:
SE.Redis contourne le contexte de synchronisation en interne (normal pour le code de bibliothèque), il ne devrait donc pas avoir le blocage
Donc, d'après ma compréhension, StackExchange.Redis devrait être indépendant de savoir si nous utilisons l'anti-pattern sync-over-async. Ce n'est tout simplement pas recommandé car cela pourrait être la cause de blocages dans un autre code .
Dans ce cas, cependant, pour autant que je sache, le blocage est vraiment à l'intérieur de StackExchange.Redis. Corrigez-moi si j'ai tort, s'il-vous plait.
J'ai trouvé que le blocage semble avoir sa source dans ProcessAsyncCompletionQueue
sur ligne 124 de CompletionManager.cs
.
Extrait de ce 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);
}
J'ai trouvé cela pendant l'impasse; activeAsyncWorkerThread
est l'un de nos threads qui attend la fin d'un appel Redis. ( notre thread = un thread de pool de threads exécutant notre code). Ainsi, la boucle ci-dessus est réputée se poursuivre pour toujours.
Sans connaître les détails, cela se sent vraiment mal; StackExchange.Redis attend un thread qu'il pense être le thread de travail asynchrone actif alors qu'il s'agit en fait d'un thread qui est tout à fait le contraire.
Je me demande si cela est dû au problème de détournement de fil (que je ne comprends pas bien)?
Les deux principales questions que j'essaie de comprendre:
Le mélange de await
et Wait()
/Result
peut-il être la cause de blocages même lors de l'exécution sans contexte de synchronisation?
Sommes-nous en train de rencontrer un bug/une limitation dans StackExchange.Redis?
D'après mes résultats de débogage, il semble que le problème soit que:
next.TryComplete(true);
... on la ligne 162 dans CompletionManager.cs
pourrait dans certaines circonstances laisser le thread courant (qui est le thread de travail asynchrone actif) s'éloigner et démarrer le traitement d'un autre code, pouvant entraîner un blocage.
Sans connaître les détails et penser à ce "fait", il semblerait logique de libérer temporairement le thread de travail asynchrone actif pendant l'invocation de TryComplete
.
Je suppose que quelque chose comme ça pourrait fonctionner:
// 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
}
}
Je suppose que mon meilleur espoir est que Marc Gravell lirait ceci et fournirait des commentaires :-)
J'ai écrit ci-dessus que notre code n'utilise pas contexte de synchronisation . Cela n'est que partiellement vrai: le code est exécuté en tant qu'application console ou en tant que rôle de travailleur Azure. Dans ces environnements SynchronizationContext.Current
est null
, c'est pourquoi j'ai écrit que nous exécutons sans contexte de synchronisation.
Cependant, après avoir lu Il s'agit du SynchronizationContext J'ai appris que ce n'est pas vraiment le cas:
Par convention, si le SynchronizationContext actuel d'un thread est nul, il a implicitement un SynchronizationContext par défaut.
Cependant, le contexte de synchronisation par défaut ne devrait pas être la cause de blocages, comme le pourrait le contexte de synchronisation basé sur l'interface utilisateur (WinForms, WPF), car il n'implique pas d'affinité de thread.
Lorsqu'un message est terminé, sa source d'achèvement est vérifiée pour savoir s'il est considéré sync safe. Si c'est le cas, l'action d'achèvement est exécutée en ligne et tout va bien.
Si ce n'est pas le cas, l'idée est d'exécuter l'action d'achèvement sur un thread de pool de threads nouvellement alloué. Cela fonctionne également très bien lorsque ConnectionMultiplexer.PreserveAsyncOrder
Est false
.
Cependant, lorsque ConnectionMultiplexer.PreserveAsyncOrder
Est true
(la valeur par défaut), ces threads de pool de threads sérialiseront leur travail à l'aide d'une file d'attente de fin et en veillant à ce qu'au plus l'un d'eux est le thread de travail asynchrone actif à tout moment.
Lorsqu'un thread devient le thread de travail asynchrone actif il continuera de l'être jusqu'à ce qu'il ait épuisé la file d'attente d'achèvement.
Le problème est que l'action de complétion est pas de synchronisation sûre (par dessus), elle est toujours exécutée sur un thread qui ne doit pas être bloqué car cela empêchera les autres messages non synchronisés de se terminer.
Notez que les autres messages en cours de réalisation avec une action de fin qui est sync safe continueront de fonctionner correctement, même si le thread de travail asynchrone actif est bloqué .
Ma "correction" suggérée (ci-dessus) ne provoquerait pas un blocage de cette manière, mais elle gâcherait cependant la notion de en préservant l'ordre d'achèvement asynchrone.
Alors peut-être que la conclusion à tirer ici est que il n'est pas sûr de mélanger await
avec Result
/Wait()
lorsque PreserveAsyncOrder
est true
, que nous exécutions sans contexte de synchronisation?
( Au moins jusqu'à ce que nous puissions utiliser .NET 4.6 et le nouveau TaskCreationOptions.RunContinuationsAsynchronously
, je suppose)
Voici les solutions de contournement que j'ai trouvées à ce problème de blocage:
Par défaut, StackExchange.Redis s'assurera que les commandes sont exécutées dans le même ordre que les messages de résultat sont reçus. Cela pourrait provoquer un blocage comme décrit dans cette question.
Désactivez ce comportement en définissant PreserveAsyncOrder
sur false
.
ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
Cela évitera les blocages et pourrait également améliorer les performances .
J'encourage tous ceux qui rencontrent des problèmes de blocage à essayer cette solution de contournement, car c'est tellement propre et simple.
Vous perdrez la garantie que les continuations asynchrones sont appelées dans le même ordre que les opérations Redis sous-jacentes sont terminées. Cependant, je ne vois pas vraiment pourquoi vous pourriez vous fier à cela.
Le blocage se produit lorsque le thread de travail asynchrone actif dans StackExchange.Redis termine une commande et lorsque la tâche d'achèvement est exécutée en ligne.
On peut empêcher l'exécution d'une tâche en ligne en utilisant un TaskScheduler
personnalisé et s'assurer que TryExecuteTaskInline
renvoie false
.
public class MyScheduler : TaskScheduler
{
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false; // Never allow inlining.
}
// TODO: Rest of TaskScheduler implementation goes here...
}
La mise en œuvre d'un bon planificateur de tâches peut être une tâche complexe. Il existe cependant des implémentations existantes dans la bibliothèque ParallelExtensionExtras ( package NuGet ) que vous pouvez utiliser ou vous inspirer.
Si votre planificateur de tâches utilise ses propres threads (pas à partir du pool de threads), il peut être judicieux d'autoriser l'inline sauf si le thread actuel provient du pool de threads. Cela fonctionnera car le thread de travail asynchrone actif dans StackExchange.Redis est toujours un thread de pool de threads.
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Don't allow inlining on a thread pool thread.
return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task);
}
Une autre idée serait d'attacher votre ordonnanceur à tous ses threads, en utilisant stockage local des threads .
private static ThreadLocal<TaskScheduler> __attachedScheduler
= new ThreadLocal<TaskScheduler>();
Assurez-vous que ce champ est attribué lorsque le thread commence à s'exécuter et effacé à la fin:
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;
}
}
Ensuite, vous pouvez autoriser l'incrustation des tâches tant qu'elle est effectuée sur un thread qui appartient au planificateur personnalisé:
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// Allow inlining on our own threads.
return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
Je devine beaucoup sur la base des informations détaillées ci-dessus et je ne connais pas le code source que vous avez en place. Il semble que vous atteigniez certaines limites internes et configurables dans .Net. Vous ne devriez pas les toucher, donc je suppose que vous ne disposez pas d'objets car ils flottent entre les threads, ce qui ne vous permettra pas d'utiliser une instruction using pour gérer proprement la durée de vie de leurs objets.
Cela détaille les limitations des requêtes HTTP. Similaire à l'ancien problème WCF lorsque vous ne supprimiez pas la connexion et que toutes les connexions WCF échouaient.
Nombre maximum de requêtes HttpWeb simultanées
C'est plus une aide au débogage, car je doute que vous utilisiez vraiment tous les ports TCP, mais de bonnes informations sur la façon de trouver le nombre de ports ouverts et où.
https://msdn.Microsoft.com/en-us/library/aa560610 (v = bts.20) .aspx