c# - Un ejemplo de asincronización/espera que causa un punto muerto
task-parallel-library deadlock (4)
Encontré algunas de las mejores prácticas para la programación asincrónica usando las palabras clave async / await de c # (soy nuevo en c # 5.0).
Uno de los consejos dados fue el siguiente:
Estabilidad: conozca sus contextos de sincronización
... Algunos contextos de sincronización son no reentrantes y de un solo subproceso. Esto significa que solo se puede ejecutar una unidad de trabajo en el contexto en un momento dado. Un ejemplo de esto es el hilo de la interfaz de usuario de Windows o el contexto de solicitud de ASP.NET. En estos contextos de sincronización de subproceso único, es fácil encerrarse en un punto muerto. Si genera una tarea de un contexto de subproceso único y luego espera esa tarea en el contexto, su código de espera puede estar bloqueando la tarea en segundo plano.
public ActionResult ActionAsync()
{
// DEADLOCK: this blocks on the async task
var data = GetDataAsync().Result;
return View(data);
}
private async Task<string> GetDataAsync()
{
// a very simple async method
var result = await MyWebService.GetDataAsync();
return result.ToString();
}
Si trato de diseccionarlo yo mismo, el hilo principal genera uno nuevo en "MyWebService.GetDataAsync ();" pero dado que el hilo principal espera allí, espera el resultado en "GetDataAsync (). Result". Mientras tanto, dicen que los datos están listos. ¿Por qué el hilo principal no continúa su lógica de continuación y devuelve un resultado de cadena de GetDataAsync ()?
¿Puede alguien explicarme por qué hay un punto muerto en el ejemplo anterior? No tengo ni idea de cuál es el problema ...
Estaba jugando con este tema nuevamente en un proyecto de MVC.Net. Cuando desee llamar a métodos asíncronos desde una vista parcial, no podrá hacer que la función PartialView sea asincrónica. Obtendrás una excepción si lo haces.
Así que, básicamente, una solución simple en el escenario donde desea llamar a un método asíncrono desde un método de sincronización, puede hacer lo siguiente:
- antes de la llamada, borre el SynchronizationContext
- hacer la llamada, no habrá más punto muerto aquí, esperar a que termine
- restaurar el SynchronizationContext
Ejemplo:
public ActionResult DisplayUserInfo(string userName)
{
// trick to prevent deadlocks of calling async method
// and waiting for on a sync UI thread.
var syncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
// this is the async call, wait for the result (!)
var model = _asyncService.GetUserInfo(Username).Result;
// restore the context
SynchronizationContext.SetSynchronizationContext(syncContext);
return PartialView("_UserInfo", model);
}
Mire aquí un ejemplo, Stephen tiene una respuesta clara para usted:
Así que esto es lo que sucede, comenzando con el método de nivel superior (Button1_Click para UI / MyController.Get para ASP.NET):
El método de nivel superior llama a GetJsonAsync (dentro del contexto UI / ASP.NET).
GetJsonAsync inicia la solicitud REST llamando a HttpClient.GetStringAsync (aún dentro del contexto).
GetStringAsync devuelve una Tarea incompleta, lo que indica que la solicitud REST no está completa.
GetJsonAsync espera la tarea devuelta por GetStringAsync. El contexto se captura y se usará para continuar ejecutando el método GetJsonAsync más adelante. GetJsonAsync devuelve una Tarea incompleta, lo que indica que el método GetJsonAsync no está completo.
El método de nivel superior se bloquea de forma síncrona en la tarea devuelta por GetJsonAsync. Esto bloquea el hilo de contexto.
... Eventualmente, la solicitud REST se completará. Esto completa la tarea devuelta por GetStringAsync.
La continuación de GetJsonAsync ahora está lista para ejecutarse, y espera que el contexto esté disponible para que pueda ejecutarse en el contexto.
Punto muerto. El método de nivel superior está bloqueando el hilo de contexto, esperando que se complete GetJsonAsync, y GetJsonAsync está esperando que el contexto sea libre para que pueda completarse. Para el ejemplo de UI, el "contexto" es el contexto de la interfaz de usuario; para el ejemplo de ASP.NET, el "contexto" es el contexto de solicitud de ASP.NET. Este tipo de interbloqueo puede deberse tanto al "contexto".
Otro enlace que deberías leer:
Otro punto principal es que no debe bloquear en las tareas, y utilizar async hasta abajo para evitar interbloqueos. Entonces será todo el bloqueo asincrónico no síncrono.
public async Task<ActionResult> ActionAsync()
{
var data = await GetDataAsync();
return View(data);
}
private async Task<string> GetDataAsync()
{
// a very simple async method
var result = await MyWebService.GetDataAsync();
return result.ToString();
}
- Hecho 1:
GetDataAsync().Result;
se ejecutará cuando la tarea devuelta porGetDataAsync()
finalice, mientras tanto, bloquea el hilo de la interfaz de usuario - Hecho 2: La continuación de la espera (
return result.ToString()
) está en cola al hilo de la interfaz de usuario para su ejecución - Hecho 3: la tarea devuelta por
GetDataAsync()
se completará cuando se ejecute su continuación en cola - Hecho 4: la continuación en cola nunca se ejecuta, porque el subproceso de interfaz de usuario está bloqueado (Hecho 1)
¡Punto muerto!
El punto muerto se puede romper con alternativas provistas para evitar el Hecho 1 o el Hecho 2.
- Evita 1,4. En lugar de bloquear el subproceso de interfaz de usuario, use
var data = await GetDataAsync()
, que permite que el subproceso de la interfaz de usuario siga funcionando - Evita 2,3. Ponga en cola la continuación de la espera de un hilo diferente que no esté bloqueado, por ejemplo, use
var data = Task.Run(GetDataAsync).Result
, que publicará la continuación en el contexto de sincronización de una cadena de subprocesos. Esto permite completar la tarea devuelta porGetDataAsync()
.
Esto se explica muy bien en un artículo de Stephen Toub , a mitad de camino, donde usa el ejemplo de DelayAsync()
.