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
enfalse
.
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
yWait()
/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 deawait
yTask.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 cualquierawait
no tiene ningún efecto sobre esto. -
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:
-
¿Podría la mezcla
await
yWait()
/Result
ser la causa de puntos muertos incluso cuando se ejecuta sin contexto de sincronización? -
¿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