Un ejemplo de async / await que provoca un interbloqueo

95

Encontré algunas de las mejores prácticas para la programación asincrónica usando c # 's async/ awaitkeywords (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 subproceso de la interfaz de usuario de Windows o el contexto de solicitud ASP.NET. En estos contextos de sincronización de un solo subproceso, es fácil bloquearse. Si genera una tarea desde un contexto de un solo subproceso, 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 analizarlo yo mismo, el hilo principal genera uno nuevo en MyWebService.GetDataAsync();, pero como el hilo principal espera allí, espera el resultado en GetDataAsync().Result. Mientras tanto, digamos 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 GetDataAsync()?

¿Puede alguien explicarme por qué hay un punto muerto en el ejemplo anterior? No tengo ni idea de cuál es el problema ...

Dror Weiss
fuente
¿Estás realmente seguro de que GetDataAsync termina sus cosas? ¿O se atasca provocando un bloqueo y no un punto muerto?
Andrey
Este es el ejemplo que se proporcionó. A mi entender, debería terminar sus cosas y tener un resultado de algún tipo listo ...
Dror Weiss
4
¿Por qué estás esperando la tarea? Debería estar esperando en su lugar porque básicamente perdió todos los beneficios del modelo asíncrono.
Toni Petrina
Para agregar al punto de @ ToniPetrina, incluso sin el problema del interbloqueo, var data = GetDataAsync().Result;hay una línea de código que nunca debe hacerse en un contexto que no debe bloquear (solicitud de interfaz de usuario o ASP.NET). Incluso si no se bloquea, está bloqueando el hilo por un período de tiempo indeterminado. Así que básicamente es un ejemplo terrible. [Debe salir del hilo de la interfaz de usuario antes de ejecutar un código como ese, o usarlo awaitallí también, como sugiere Toni.]
ToolmakerSteve

Respuestas:

81

Eche un vistazo a este ejemplo , Stephen tiene una respuesta clara para usted:

Entonces esto es lo que sucede, comenzando con el método de nivel superior ( Button1_Clickpara UI / MyController.Getpara ASP.NET):

  1. Las llamadas al método de nivel superior GetJsonAsync(dentro del contexto UI / ASP.NET).

  2. GetJsonAsyncinicia la solicitud REST llamando HttpClient.GetStringAsync(aún dentro del contexto).

  3. GetStringAsyncdevuelve un incompleto Task, lo que indica que la solicitud REST no está completa.

  4. GetJsonAsyncespera el Taskdevuelto GetStringAsync. El contexto se captura y se utilizará para seguir ejecutando el GetJsonAsyncmétodo más adelante. GetJsonAsyncdevuelve un incompleto Task, lo que indica que el GetJsonAsyncmétodo no está completo.

  5. El método de nivel superior bloquea sincrónicamente el Taskdevuelto por GetJsonAsync. Esto bloquea el hilo de contexto.

  6. ... Eventualmente, la solicitud REST se completará. Esto completa el Taskque fue devuelto por GetStringAsync.

  7. La continuación de GetJsonAsyncahora está lista para ejecutarse y espera a que el contexto esté disponible para que pueda ejecutarse en el contexto.

  8. Deadlock . El método de nivel superior está bloqueando el hilo de contexto, esperando GetJsonAsyncque se complete y GetJsonAsyncestá esperando que el contexto esté libre para que pueda completarse. Para el ejemplo de la interfaz de usuario, 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 a cualquier "contexto".

Otro enlace que debería leer: ¡ Espera, interfaz de usuario y puntos muertos! ¡Oh mi!

cuongle
fuente
21
  • Hecho 1: GetDataAsync().Result;se ejecutará cuando la tarea devuelta por se GetDataAsync()complete, mientras tanto, bloquea el hilo de la interfaz de usuario
  • Hecho 2: la continuación de await ( return result.ToString()) se pone en cola en el 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 la interfaz de usuario está bloqueado (Hecho 1)

¡Punto muerto!

El punto muerto se puede romper con las alternativas proporcionadas para evitar el Hecho 1 o el Hecho 2.

  • Evite 1,4. En lugar de bloquear el subproceso de la interfaz de usuario, utilice var data = await GetDataAsync(), que permite que el subproceso de la interfaz de usuario siga ejecutándose
  • Evite 2,3. Ponga en cola la continuación de la espera a un subproceso diferente que no esté bloqueado, por ejemplo var data = Task.Run(GetDataAsync).Result, use , que publicará la continuación en el contexto de sincronización de un subproceso de grupo de subprocesos. Esto permite GetDataAsync()que se complete la tarea devuelta por .

Esto se explica muy bien en un artículo de Stephen Toub , aproximadamente a la mitad de donde usa el ejemplo de DelayAsync().

Phillip Ngan
fuente
Respecto a var data = Task.Run(GetDataAsync).Resulteso, eso es nuevo para mí. Siempre pensé que el exterior .Resultestará disponible tan pronto como GetDataAsyncllegue la primera espera , así dataque siempre lo estará default. Interesante.
Nawfal
19

Estaba jugando con este problema nuevamente en un proyecto ASP.NET MVC. Cuando desee llamar a asyncmétodos desde a PartialView, no puede realizar el PartialView async. Obtendrá una excepción si lo hace.

Puede utilizar la siguiente solución alternativa sencilla en el escenario en el que desea llamar a un asyncmétodo desde un método de sincronización:

  1. Antes de la llamada, borre el SynchronizationContext
  2. Haz la llamada, no habrá más puntos muertos aquí, espera a que termine
  3. 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);
}
Herre Kuijpers
fuente
3

Otro punto principal es que no debe bloquear las Tareas y usar async hasta el final para evitar interbloqueos. Entonces todo será bloqueo asíncrono, 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();
}
marvelTracker
fuente
6
¿Qué sucede si quiero que se bloquee el hilo principal (UI) hasta que finalice la tarea? ¿O en una aplicación de consola, por ejemplo? Digamos que quiero usar HttpClient, que solo admite async ... ¿Cómo lo uso sincrónicamente sin el riesgo de un punto muerto ? Esto debe ser posible. Si WebClient se puede usar de esa manera (debido a que tiene métodos de sincronización) y funciona perfectamente, ¿por qué no se puede hacer también con HttpClient?
Dexter
Vea la respuesta de Philip Ngan arriba (sé que esto se publicó después de este comentario): ponga en cola la continuación de la espera en un hilo diferente que no esté bloqueado, por ejemplo, use var data = Task.Run (GetDataAsync) .Result
Jeroen
@Dexter - re " ¿Qué pasa si quiero que el hilo principal (UI) se bloquee hasta que finalice la tarea? " - ¿De verdad desea que el hilo de la interfaz de usuario se bloquee, lo que significa que el usuario no puede hacer nada, ni siquiera puede cancelar? ¿Es que no desea continuar con el método en el que se encuentra? "await" o "Task.ContinueWith" manejan el último caso.
ToolmakerSteve
@ToolmakerSteve, por supuesto, no quiero continuar con el método. Pero simplemente no puedo usar await porque tampoco puedo usar async hasta el final: HttpClient se invoca en main , que por supuesto no puede ser async. Y luego mencioné hacer todo esto en una aplicación de consola; en este caso, quiero exactamente lo primero, no quiero que mi aplicación sea ​​de varios subprocesos. Bloquea todo .
Dexter
-1

Una solución que encontré es usar un Joinmétodo de extensión en la tarea antes de pedir el resultado.

El código se ve así:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

Donde el método de unión es:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

No estoy lo suficientemente interesado en el dominio para ver los inconvenientes de esta solución (si corresponde)

Orace
fuente