C# Language ASP.NET Configure Await


Ejemplo

Cuando ASP.NET maneja una solicitud, se asigna un subproceso desde el grupo de subprocesos y se crea un contexto de solicitud . El contexto de solicitud contiene información sobre la solicitud actual a la que se puede acceder a través de la propiedad estática HttpContext.Current . El contexto de solicitud para la solicitud se asigna al hilo que maneja la solicitud.

Un contexto de solicitud dado solo puede estar activo en un hilo a la vez .

Cuando la ejecución llega a la await , el subproceso que maneja una solicitud se devuelve al grupo de subprocesos mientras se ejecuta el método asíncrono y el contexto de la solicitud es libre para que otro subproceso lo utilice.

public async Task<ActionResult> Index()
{
    // Execution on the initially assigned thread
    var products = await dbContext.Products.ToListAsync();

    // Execution resumes on a "random" thread from the pool
    // Execution continues using the original request context.
    return View(products);
}

Cuando la tarea se completa, el grupo de hilos asigna otro hilo para continuar la ejecución de la solicitud. El contexto de solicitud se asigna a este hilo. Este puede o no ser el hilo original.

Bloqueando

Cuando se espera el resultado de una llamada a un método async , pueden surgir puntos muertos sincrónicos . Por ejemplo, el siguiente código resultará en un interbloqueo cuando se IndexSync() :

public async Task<ActionResult> Index()
{
    // Execution on the initially assigned thread
    List<Product> products = await dbContext.Products.ToListAsync();

    // Execution resumes on a "random" thread from the pool
    return View(products);
}

public ActionResult IndexSync()
{
    Task<ActionResult> task = Index();

    // Block waiting for the result synchronously
    ActionResult result = Task.Result;

    return result;       
}

Esto se debe a que, de forma predeterminada, la tarea esperada, en este caso db.Products.ToListAsync() capturará el contexto (en el caso de ASP.NET el contexto de la solicitud) e intentará usarlo una vez que se haya completado.

Cuando toda la pila de llamadas es asíncrona, no hay problema porque, una vez await se alcanza la await el hilo original se libera, liberando el contexto de la solicitud.

Cuando Task.Result sincrónica utilizando Task.Result o Task.Wait() (u otros métodos de bloqueo), el subproceso original todavía está activo y conserva el contexto de la solicitud. El método esperado aún funciona de forma asíncrona y una vez que la devolución de llamada intenta ejecutarse, es decir, una vez que la tarea esperada ha regresado, intenta obtener el contexto de la solicitud.

Por lo tanto, el interbloqueo surge porque mientras el subproceso de bloqueo con el contexto de la solicitud está esperando a que se complete la operación asíncrona, la operación asíncrona está tratando de obtener el contexto de la solicitud para poder completarla.

ConfigureAwait

De forma predeterminada, las llamadas a una tarea esperada capturarán el contexto actual e intentarán reanudar la ejecución en el contexto una vez completado.

Al utilizar ConfigureAwait(false) esto se puede evitar y los puntos muertos se pueden evitar.

public async Task<ActionResult> Index()
{
    // Execution on the initially assigned thread
    List<Product> products = await dbContext.Products.ToListAsync().ConfigureAwait(false);

    // Execution resumes on a "random" thread from the pool without the original request context
    return View(products);
}

public ActionResult IndexSync()
{
    Task<ActionResult> task = Index();

    // Block waiting for the result synchronously
    ActionResult result = Task.Result;

    return result;       
}

Esto puede evitar puntos muertos cuando es necesario bloquear en código asíncrono, sin embargo, esto conlleva el costo de perder el contexto en la continuación (código después de la llamada a la espera).

En ASP.NET, esto significa que si su código después de una llamada await someTask.ConfigureAwait(false); intenta acceder a la información desde el contexto, por ejemplo, HttpContext.Current.User entonces la información se ha perdido. En este caso, el HttpContext.Current es nulo. Por ejemplo:

public async Task<ActionResult> Index()
{
    // Contains information about the user sending the request
    var user = System.Web.HttpContext.Current.User;

    using (var client = new HttpClient())
    {
        await client.GetAsync("http://google.com").ConfigureAwait(false);
    }

    // Null Reference Exception, Current is null
    var user2 = System.Web.HttpContext.Current.User;

    return View();
}

Si se usa ConfigureAwait(true) (equivalente a no tener ningún ConfigureAwait), entonces el user y el user2 se llenan con los mismos datos.

Por este motivo, a menudo se recomienda usar ConfigureAwait(false) en el código de la biblioteca donde ya no se usa el contexto.