C# Language
Asíncrono-espera
Buscar..
Introducción
En C #, un método declarado async
no se bloqueará dentro de un proceso síncrono, en caso de que esté utilizando operaciones basadas en E / S (por ejemplo, acceso web, trabajo con archivos, ...). El resultado de dichos métodos marcados asíncronos puede esperarse mediante el uso de la palabra clave await
.
Observaciones
Un método async
puede devolver void
, Task
o Task<T>
.
La Task
tipo de retorno esperará a que finalice el método y el resultado se void
. Task<T>
devolverá un valor del tipo T
después de que se complete el método.
async
métodos async
deben devolver Task
o Task<T>
, en lugar de void
, en casi todas las circunstancias. async void
métodos de async void
no se pueden await
, lo que conduce a una variedad de problemas. El único escenario en el que un async
debe devolver un void
es en el caso de un controlador de eventos.
async
/ await
funciona al transformar su método async
en una máquina de estado. Lo hace creando una estructura detrás de escena que almacena el estado actual y cualquier contexto (como las variables locales), y expone un método MoveNext()
para avanzar los estados (y ejecutar cualquier código asociado) cada vez que se completa un proceso esperado.
Simples llamadas consecutivas
public async Task<JobResult> GetDataFromWebAsync()
{
var nextJob = await _database.GetNextJobAsync();
var response = await _httpClient.GetAsync(nextJob.Uri);
var pageContents = await response.Content.ReadAsStringAsync();
return await _database.SaveJobResultAsync(pageContents);
}
Lo principal a tener en cuenta es que, si bien todos los await
método -ed se llama de forma asincrónica - y por el tiempo de la llamada del control es cedido de nuevo al sistema - el flujo en el interior del método es lineal y no requiere ningún tratamiento especial debido a las asincronía. Si alguno de los métodos llamados falla, la excepción se procesará "como se esperaba", lo que en este caso significa que la ejecución del método se anulará y la excepción subirá la pila.
Probar / Atrapar / Finalmente
A partir de C # 6.0, la palabra clave await
ahora se puede usar dentro de un bloque catch
y finally
.
try {
var client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
await client.LogExceptionAsync();
throw;
} finally {
await client.CloseAsync();
}
Antes de C # 6.0, tendría que hacer algo como lo siguiente. Tenga en cuenta que 6.0 también limpió las comprobaciones nulas con el operador de propagación nula .
AsynClient client;
MyException caughtException;
try {
client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
caughtException = ex;
}
if (client != null) {
if (caughtException != null) {
await client.LogExceptionAsync();
}
await client.CloseAsync();
if (caughtException != null) throw caughtException;
}
Tenga en cuenta que si espera una tarea no creada por async
(p. Ej., Una tarea creada por Task.Run
), algunos depuradores pueden interrumpir las excepciones generadas por la tarea, incluso cuando parece ser manejada por el try / catch que la rodea. Esto sucede porque el depurador considera que no está manejado con respecto al código de usuario. En Visual Studio, hay una opción llamada "Just My Code" , que se puede desactivar para evitar que el depurador se rompa en tales situaciones.
Configuración de Web.config para apuntar a 4.5 para un comportamiento asíncrono correcto.
El web.config system.web.httpRuntime debe apuntar a 4.5 para garantizar que el subproceso inquiline el contexto de la solicitud antes de reanudar su método asíncrono.
<httpRuntime targetFramework="4.5" />
Async y espera tienen un comportamiento indefinido en ASP.NET antes de 4.5. Async / await se reanudará en un hilo arbitrario que puede no tener el contexto de solicitud. Las aplicaciones bajo carga fallarán aleatoriamente con excepciones de referencia nulas que accedan a HttpContext después de la espera. Usar HttpContext.Current en WebApi es peligroso debido a async
Llamadas concurrentes
Es posible esperar múltiples llamadas simultáneamente invocando primero las tareas que se pueden esperar y luego esperándolas.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await firstTask;
await secondTask;
}
Alternativamente, Task.WhenAll
se puede usar para agrupar múltiples tareas en una sola Task
, que se completa cuando todas sus tareas pasadas están completas.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await Task.WhenAll(firstTask, secondTask);
}
También puedes hacer esto dentro de un bucle, por ejemplo:
List<Task> tasks = new List<Task>();
while (something) {
// do stuff
Task someAsyncTask = someAsyncMethod();
tasks.Add(someAsyncTask);
}
await Task.WhenAll(tasks);
Para obtener resultados de una tarea después de esperar varias tareas con Tarea. Cuando todo, simplemente vuelva a esperar la tarea. Ya que la tarea ya está completada, solo devolverá el resultado
var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();
await Task.WhenAll(task1, task2);
var result = await task2;
Además, Task.WhenAny
se puede usar para ejecutar múltiples tareas en paralelo, como Task.WhenAll
arriba, con la diferencia de que este método se completará cuando se complete cualquiera de las tareas proporcionadas.
public async Task RunConcurrentTasksWhenAny()
{
var firstTask = TaskOperation("#firstTask executed");
var secondTask = TaskOperation("#secondTask executed");
var thirdTask = TaskOperation("#thirdTask executed");
await Task.WhenAny(firstTask, secondTask, thirdTask);
}
La Task
devuelta por RunConcurrentTasksWhenAny
se completará cuando se complete cualquiera de firstTask
, secondTask
o thirdTask
.
Espera operador y palabra clave asíncrona.
await
operador y palabra clave async
se unen:
El método asíncrono en el que esperan se utiliza debe ser modificado por la palabra clave asíncrono.
Lo contrario no siempre es cierto: puede marcar un método como async
sin usar await
en su cuerpo.
Lo que await
realmente es suspender la ejecución del código hasta que se complete la tarea esperada; Cualquier tarea puede ser esperada.
Nota: no puede esperar por un método asíncrono que no devuelve nada (vacío).
En realidad, la palabra 'suspender' es un poco engañosa porque no solo se detiene la ejecución, sino que el hilo puede quedar libre para ejecutar otras operaciones. Bajo el capó, await
se implementa con un poco de magia de compilación: divide un método en dos partes: antes y después de await
. La última parte se ejecuta cuando se completa la tarea esperada.
Si ignoramos algunos detalles importantes, el compilador aproximadamente lo hace por usted:
public async Task<TResult> DoIt()
{
// do something and acquire someTask of type Task<TSomeResult>
var awaitedResult = await someTask;
// ... do something more and produce result of type TResult
return result;
}
se convierte en:
public Task<TResult> DoIt()
{
// ...
return someTask.ContinueWith(task => {
var result = ((Task<TSomeResult>)task).Result;
return DoIt_Continuation(result);
});
}
private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
// ...
}
Cualquier método habitual puede convertirse en asíncrono de la siguiente manera:
await Task.Run(() => YourSyncMethod());
Esto puede ser ventajoso cuando necesita ejecutar un método de ejecución prolongada en el subproceso de la interfaz de usuario sin congelar la interfaz de usuario.
Pero hay un comentario muy importante aquí: asíncrono no siempre significa concurrente (paralelo o incluso multiproceso). Incluso en un solo hilo, async
- await
todavía permite el código asíncrono. Por ejemplo, vea este programador de tareas personalizado. Un programador de tareas tan "loco" puede simplemente convertir las tareas en funciones que se llaman dentro del procesamiento del bucle de mensajes.
Necesitamos preguntarnos: ¿Qué hilo ejecutará la continuación de nuestro método DoIt_Continuation
?
Por defecto, el operador a la await
programa la ejecución de continuación con el contexto de sincronización actual. Esto significa que, de forma predeterminada, WinForms y WPF se ejecutan en el subproceso de la interfaz de usuario. Si, por algún motivo, necesita cambiar este comportamiento, use el método Task.ConfigureAwait()
:
await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);
Devolviendo una tarea sin esperar
Los métodos que realizan operaciones asíncronas no tienen que usar await
si:
- Solo hay una llamada asíncrona dentro del método.
- La llamada asíncrona se encuentra al final del método.
- La excepción de captura / manejo que puede ocurrir dentro de la Tarea no es necesaria
Considere este método que devuelve una Task
:
public async Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return await dataStore.GetByKeyAsync(lookupKey);
}
Si GetByKeyAsync
tiene la misma firma que GetUserAsync
(que devuelve una Task<User>
), el método se puede simplificar:
public Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return dataStore.GetByKeyAsync(lookupKey);
}
En este caso, el método no necesita estar marcado como async
, a pesar de que está realizando una operación asíncrona. La tarea devuelta por GetByKeyAsync
se pasa directamente al método de llamada, donde se await
.
Importante : Devolver la Task
lugar de esperarla, cambia el comportamiento de excepción del método, ya que no lanzará la excepción dentro del método que inicia la tarea sino en el método que la espera.
public Task SaveAsync()
{
try {
return dataStore.SaveChangesAsync();
}
catch(Exception ex)
{
// this will never be called
logger.LogException(ex);
}
}
// Some other code calling SaveAsync()
// If exception happens, it will be thrown here, not inside SaveAsync()
await SaveAsync();
Esto mejorará el rendimiento, ya que ahorrará al compilador la generación de una máquina de estado asíncrono adicional.
El bloqueo en el código asíncrono puede causar interbloqueos
Es una mala práctica bloquear las llamadas asíncronas, ya que puede provocar interbloqueos en entornos que tienen un contexto de sincronización. La mejor práctica es usar async / await "hasta el final". Por ejemplo, el siguiente código de Windows Forms provoca un interbloqueo:
private async Task<bool> TryThis()
{
Trace.TraceInformation("Starting TryThis");
await Task.Run(() =>
{
Trace.TraceInformation("In TryThis task");
for (int i = 0; i < 100; i++)
{
// This runs successfully - the loop runs to completion
Trace.TraceInformation("For loop " + i);
System.Threading.Thread.Sleep(10);
}
});
// This never happens due to the deadlock
Trace.TraceInformation("About to return");
return true;
}
// Button click event handler
private void button1_Click(object sender, EventArgs e)
{
// .Result causes this to block on the asynchronous call
bool result = TryThis().Result;
// Never actually gets here
Trace.TraceInformation("Done with result");
}
Esencialmente, una vez que se completa la llamada asíncrona, espera que el contexto de sincronización esté disponible. Sin embargo, el controlador de eventos "se mantiene" en el contexto de sincronización mientras espera que se TryThis()
método TryThis()
, lo que provoca una espera circular.
Para arreglar esto, el código debe ser modificado para
private async void button1_Click(object sender, EventArgs e)
{
bool result = await TryThis();
Trace.TraceInformation("Done with result");
}
Nota: los controladores de eventos son el único lugar donde se debe usar el async void
(porque no puede esperar un método de async void
).
Async / await solo mejorará el rendimiento si permite que la máquina realice trabajo adicional
Considere el siguiente código:
public async Task MethodA()
{
await MethodB();
// Do other work
}
public async Task MethodB()
{
await MethodC();
// Do other work
}
public async Task MethodC()
{
// Or await some other async work
await Task.Delay(100);
}
Esto no funcionará mejor que
public void MethodA()
{
MethodB();
// Do other work
}
public void MethodB()
{
MethodC();
// Do other work
}
public void MethodC()
{
Thread.Sleep(100);
}
El propósito principal de async / await es permitir que la máquina realice trabajo adicional, por ejemplo, para permitir que el subproceso que realiza la llamada realice otro trabajo mientras espera el resultado de alguna operación de E / S. En este caso, al subproceso de llamada nunca se le permite hacer más trabajo de lo que hubiera podido hacer de otra manera, por lo que no hay ganancia de rendimiento al simplemente llamar a MethodA()
, MethodB()
y MethodC()
sincrónica.