c# asynchronous deadlock stackexchange.redis

c# - Punto muerto al acceder a StackExchange.Redis



asynchronous deadlock (2)

Estas son las soluciones que he encontrado para este problema de punto muerto:

Solución # 1

Por defecto, StackExchange.Redis se asegurará de que los comandos se completen en el mismo orden en que se reciben los mensajes resultantes. Esto podría causar un punto muerto como se describe en esta pregunta.

Deshabilite ese comportamiento estableciendo PreserveAsyncOrder en false .

ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;

Esto evitará puntos muertos y también podría mejorar el rendimiento .

Recomiendo a cualquiera que se encuentre con problemas de punto muerto que pruebe esta solución, ya que es muy simple y limpio.

Perderá la garantía de que las invocaciones asíncronas se invocan en el mismo orden en que se completan las operaciones subyacentes de Redis. Sin embargo, realmente no veo por qué eso es algo en lo que confiarías.

Solución # 2

El punto muerto se produce cuando el subproceso de trabajo asíncrono activo en StackExchange.Redis completa un comando y cuando la tarea de finalización se ejecuta en línea.

Se puede evitar que una tarea se ejecute en línea utilizando un TaskScheduler personalizado y asegurarse de que TryExecuteTaskInline devuelva 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... }

Implementar un buen planificador de tareas puede ser una tarea compleja. Sin embargo, existen implementaciones existentes en la biblioteca ParallelExtensionExtras ( paquete NuGet ) que puede usar o inspirarse.

Si su programador de tareas usaría sus propios subprocesos (no del conjunto de subprocesos), entonces sería una buena idea permitir la inclusión en línea a menos que el subproceso actual sea del conjunto de subprocesos. Esto funcionará porque el subproceso de trabajo asíncrono activo en StackExchange.Redis siempre es un subproceso de grupo de subprocesos.

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Don''t allow inlining on a thread pool thread. return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task); }

Otra idea sería adjuntar su programador a todos sus hilos, utilizando el almacenamiento local de hilos .

private static ThreadLocal<TaskScheduler> __attachedScheduler = new ThreadLocal<TaskScheduler>();

Asegúrese de que este campo se asigne cuando el subproceso comience a ejecutarse y se borre cuando se complete:

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; } }

Luego, puede permitir la inclusión de tareas siempre que se realice en un subproceso que sea "propiedad" del programador personalizado:

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Allow inlining on our own threads. return __attachedScheduler.Value == this && this.TryExecuteTask(task); }

Me encuentro con una situación de bloqueo cuando llamo a StackExchange.Redis .

No sé exactamente lo que está sucediendo, lo cual es muy frustrante, y agradecería cualquier aporte que pueda ayudar a resolver o solucionar este problema.

En caso de que también tenga este problema y no quiera leer todo esto; Le sugiero que intente configurar PreserveAsyncOrder en false .

ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;

Hacerlo probablemente resolverá el tipo de punto muerto sobre el que se trata este Q&A y también podría mejorar el rendimiento.

Nuestra configuración

  • El código se ejecuta como una aplicación de consola o como un rol de trabajador de Azure.
  • Expone una API REST usando HttpMessageHandler para que el punto de entrada sea asíncrono.
  • Algunas partes del código tienen afinidad de hilo (es propiedad de un solo hilo y debe ser ejecutado por este).
  • Algunas partes del código son solo asíncronas.
  • Estamos haciendo los sync-over-async y async-over-sync . (mezclando await y Wait() / Result ).
  • Solo estamos utilizando métodos asincrónicos cuando accedemos a Redis.
  • Estamos usando StackExchange.Redis 1.0.450 para .NET 4.5.

Punto muerto

Cuando se inicia la aplicación / servicio, se ejecuta normalmente durante un tiempo y luego, de repente (casi) todas las solicitudes entrantes dejan de funcionar, nunca producen una respuesta. Todas esas solicitudes están bloqueadas esperando que se complete una llamada a Redis.

Curiosamente, una vez que se produce el punto muerto, cualquier llamada a Redis se bloqueará, pero solo si esas llamadas se realizan desde una solicitud API entrante, que se ejecuta en el grupo de subprocesos.

También estamos haciendo llamadas a Redis desde subprocesos de fondo de baja prioridad, y estas llamadas continúan funcionando incluso después de que se produjo el punto muerto.

Parece que solo se producirá un punto muerto cuando se llame a Redis en un subproceso de grupo de subprocesos. Ya no creo que esto se deba al hecho de que esas llamadas se realizan en un subproceso de grupo de subprocesos. Más bien, parece que cualquier llamada asincrónica de Redis sin continuación, o con una continuación segura de sincronización , continuará funcionando incluso después de que se haya producido la situación de bloqueo. (Vea lo que creo que sucede a continuación)

Relacionado

  • StackExchange.Redis Deadlocking

    Task.Result muerto causado por la mezcla de await y Task.Result (sync-over-async, como nosotros). Pero nuestro código se ejecuta sin contexto de sincronización, por lo que no se aplica aquí, ¿verdad?

  • ¿Cómo mezclar de forma segura la sincronización y el código asíncrono?

    Sí, no deberíamos estar haciendo eso. Pero lo hacemos, y tendremos que seguir haciéndolo por un tiempo. Gran cantidad de código que debe migrarse al mundo asíncrono.

    Nuevamente, no tenemos un contexto de sincronización, por lo que esto no debería estar causando puntos muertos, ¿verdad?

    La ConfigureAwait(false) antes de cualquier await no tiene ningún efecto sobre esto.

  • Excepción de tiempo de espera después de comandos asincrónicos y Task.WhenAny espera en StackExchange.Redis

    Este es el problema del secuestro de hilos. ¿Cuál es la situación actual en esto? ¿Podría ser este el problema aquí?

  • La llamada asincrónica StackExchange.Redis se bloquea

    De la respuesta de Marc:

    ... mezclar esperar y esperar no es una buena idea. Además de los puntos muertos, esto es "sincronización sobre asíncrono", un antipatrón.

    Pero también dice:

    SE.Redis omite el contexto de sincronización internamente (normal para el código de la biblioteca), por lo que no debería tener el punto muerto

    Entonces, según tengo entendido, StackExchange.Redis debería ser independiente de si estamos usando el antipatrón sync-over-async . Simplemente no se recomienda, ya que podría ser la causa de puntos muertos en otro código.

    Sin embargo, en este caso, por lo que puedo ver, el punto muerto está realmente dentro de StackExchange.Redis. Por favor corrígeme si estoy equivocado.

Resultados de depuración

Descubrí que el punto muerto parece tener su origen en ProcessAsyncCompletionQueue en la línea 124 de CompletionManager.cs .

Fragmento de ese código:

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); }

Lo he encontrado durante el punto muerto; activeAsyncWorkerThread es uno de nuestros hilos que está esperando que se complete una llamada de Redis. ( nuestro hilo = un hilo de grupo de hilos que ejecuta nuestro código ). Por lo tanto, se considera que el ciclo anterior continuará para siempre.

Sin conocer los detalles, esto seguramente se siente mal; StackExchange.Redis está esperando un subproceso que cree que es el subproceso de trabajo asíncrono activo, mientras que en realidad es un subproceso que es todo lo contrario de eso.

Me pregunto si esto se debe al problema de secuestro de hilos (que no entiendo completamente).

¿Qué hacer?

Las dos preguntas principales que estoy tratando de resolver:

  1. ¿Podría la mezcla await y Wait() / Result ser la causa de puntos muertos incluso cuando se ejecuta sin contexto de sincronización?

  2. ¿Nos encontramos con un error / limitación en StackExchange.Redis?

¿Una posible solución?

De mis hallazgos de depuración parece que el problema es que:

next.TryComplete(true);

... en la línea 162 en CompletionManager.cs , en algunas circunstancias, podría dejar que el hilo actual (que es el hilo de trabajo asíncrono activo ) se desvíe y comience a procesar otro código, posiblemente causando un punto muerto.

Sin conocer los detalles y solo pensar en este "hecho", parecería lógico liberar temporalmente el hilo de trabajo asíncrono activo durante la invocación TryComplete .

Supongo que algo como esto podría funcionar:

// 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 } }

Supongo que mi mejor esperanza es que Marc Gravell lea esto y proporcione algunos comentarios :-)

Sin contexto de sincronización = El contexto de sincronización predeterminado

He escrito anteriormente que nuestro código no usa un contexto de sincronización . Esto es solo parcialmente cierto: el código se ejecuta como una aplicación de consola o como un rol de trabajador de Azure. En estos entornos, SynchronizationContext.Current es null , por eso escribí que estamos ejecutando sin contexto de sincronización.

Sin embargo, después de leer It''s All About the SynchronizationContext he aprendido que este no es realmente el caso:

Por convención, si el SynchronizationContext actual de un subproceso es nulo, implícitamente tiene un SynchronizationContext predeterminado.

Sin embargo, el contexto de sincronización predeterminado no debería ser la causa de puntos muertos, como podría hacerlo el contexto de sincronización basado en UI (WinForms, WPF), porque no implica afinidad de subprocesos.

Lo que creo que pasa

Cuando se completa un mensaje, se verifica su origen de finalización para ver si se considera seguro para la sincronización . Si es así, la acción de finalización se ejecuta en línea y todo está bien.

Si no es así, la idea es ejecutar la acción de finalización en un subproceso de grupo de subprocesos recién asignado. Esto también funciona bien cuando ConnectionMultiplexer.PreserveAsyncOrder es false .

Sin embargo, cuando ConnectionMultiplexer.PreserveAsyncOrder es true (el valor predeterminado), esos subprocesos del grupo de subprocesos serializarán su trabajo utilizando una cola de finalización y asegurando que, como máximo, uno de ellos sea el subproceso de trabajo asíncrono activo en cualquier momento.

Cuando un subproceso se convierte en el subproceso de trabajo asíncrono activo , seguirá siendo así hasta que se haya agotado la cola de finalización .

El problema es que la acción de finalización no es segura para la sincronización (desde arriba), aún así se ejecuta en un hilo que no debe bloquearse ya que evitará que se completen otros mensajes no seguros para la sincronización .

Observe que otros mensajes que se están completando con una acción de finalización que es segura para la sincronización continuarán funcionando bien, incluso aunque el subproceso de trabajo asíncrono activo esté bloqueado.

Mi "solución" sugerida (arriba) no causaría un punto muerto de esta manera, sin embargo, interferiría con la noción de preservar el orden de finalización asíncrono .

Entonces, ¿tal vez la conclusión a hacer aquí es que no es seguro mezclar await con Result / Wait() cuando PreserveAsyncOrder es true , sin importar si estamos ejecutando sin contexto de sincronización?

( Al menos hasta que podamos usar .NET 4.6 y las nuevas TaskCreationOptions.RunContinuationsAsynchronously , supongo )


Supongo mucho en función de la información detallada anterior y no conozco el código fuente que tiene en su lugar. Parece que puede estar alcanzando algunos límites internos y configurables en .Net. No debería golpearlos, así que supongo que no está desechando objetos ya que están flotando entre hilos que no le permitirán usar una declaración de uso para manejar limpiamente la vida útil de sus objetos.

Esto detalla las limitaciones en las solicitudes HTTP. Similar al viejo problema WCF cuando no eliminaste la conexión y luego todas las conexiones WCF fallarían.

Número máximo de HttpWebRequests concurrentes

Esto es más una ayuda de depuración, ya que dudo que realmente esté utilizando todos los puertos TCP, pero hay buena información sobre cómo encontrar cuántos puertos abiertos tiene y hacia dónde.

https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx