¿Existe alguna diferencia conceptual entre las siguientes dos piezas de código?
async Task TestAsync()
{
await Task.Run(() => DoSomeWork());
}
y
Task TestAsync()
{
return Task.Run(() => DoSomeWork());
}
¿El código generado también es diferente?
EDITAR: Para evitar confusiones con Task.Run
, un caso similar:
async Task TestAsync()
{
await Task.Delay(1000);
}
y
Task TestAsync()
{
return Task.Delay(1000);
}
ACTUALIZACIÓN TARDÍA: además de la respuesta aceptada, también hay una diferencia en cómo LocalCallContext
se maneja: CallContext.LogicalGetData se restaura incluso cuando no hay asincronía. ¿Por qué?
c#
async-await
avo
fuente
fuente
await
/async
en absoluto :)Respuestas:
Una diferencia importante está en la propagación de excepciones. Una excepción, arrojado dentro de un
async Task
método, se almacena en el vueltoTask
objeto y permanece latente hasta que la tarea se observa a través deawait task
,task.Wait()
,task.Result
otask.GetAwaiter().GetResult()
. Se propaga de esta manera incluso si se lanza desde la parte sincrónica delasync
método.Considere el siguiente código, donde
OneTestAsync
y seAnotherTestAsync
comportan de manera bastante diferente:static async Task OneTestAsync(int n) { await Task.Delay(n); } static Task AnotherTestAsync(int n) { return Task.Delay(n); } // call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest static void DoTestAsync(Func<int, Task> whatTest, int n) { Task task = null; try { // start the task task = whatTest(n); // do some other stuff, // while the task is pending Console.Write("Press enter to continue"); Console.ReadLine(); task.Wait(); } catch (Exception ex) { Console.Write("Error: " + ex.Message); } }
Si llamo
DoTestAsync(OneTestAsync, -2)
, produce la siguiente salida:Tenga en cuenta que tuve que presionar Enterpara verlo.
Ahora, si llamo
DoTestAsync(AnotherTestAsync, -2)
, el flujo de trabajo del código internoDoTestAsync
es bastante diferente, al igual que la salida. Esta vez, no me pidieron que presione Enter:En ambos casos se
Task.Delay(-2)
lanza al principio, mientras valida sus parámetros. Este podría ser un escenario inventado, pero en teoríaTask.Delay(1000)
también puede producirse , por ejemplo, cuando falla la API del temporizador del sistema subyacente.En una nota al margen, la lógica de propagación de errores es aún diferente para los
async void
métodos (a diferencia de losasync Task
métodos). Una excepción generada dentro de unasync void
método se volverá a lanzar inmediatamente en el contexto de sincronización del hilo actual (víaSynchronizationContext.Post
), si el hilo actual tiene uno (SynchronizationContext.Current != null)
. De lo contrario, se volverá a lanzar víaThreadPool.QueueUserWorkItem
). La persona que llama no tiene la oportunidad de manejar esta excepción en el mismo marco de pila.Publiqué algunos detalles más sobre el comportamiento de manejo de excepciones de TPL aquí y aquí .
P : ¿Es posible imitar el comportamiento de propagación de excepciones de los
async
métodos para métodos noTask
basados en asíncronos , de modo que este último no se ejecute en el mismo marco de pila?R : Si realmente es necesario, entonces sí, hay un truco para eso:
// async async Task<int> MethodAsync(int arg) { if (arg < 0) throw new ArgumentException("arg"); // ... return 42 + arg; } // non-async Task<int> MethodAsync(int arg) { var task = new Task<int>(() => { if (arg < 0) throw new ArgumentException("arg"); // ... return 42 + arg; }); task.RunSynchronously(TaskScheduler.Default); return task; }
Sin embargo, tenga en cuenta que bajo ciertas condiciones (como cuando está demasiado profundo en la pila),
RunSynchronously
aún podría ejecutarse de forma asincrónica.Otra diferencia notable es que la versión
async
/await
es más propensa a bloquearse en un contexto de sincronización no predeterminado . Por ejemplo, lo siguiente se bloqueará en una aplicación WinForms o WPF:static async Task TestAsync() { await Task.Delay(1000); } void Form_Load(object sender, EventArgs e) { TestAsync().Wait(); // dead-lock here }
Cámbielo a una versión no asíncrona y no se bloqueará:
Task TestAsync() { return Task.Delay(1000); }
La naturaleza del bloqueo está bien explicada por Stephen Cleary en su blog .
fuente
return Task.Run()
yawait Task.Run(); return
, en lugar deawait Task.Run().ConfigureAwait(false); return
Estoy confundido por esta pregunta. Permítame intentar aclararlo respondiendo a su pregunta con otra pregunta. ¿Cuál es la diferencia entre?
Func<int> MakeFunction() { Func<int> f = ()=>1; return ()=>f(); }
y
Func<int> MakeFunction() { return ()=>1; }
?
Cualquiera que sea la diferencia entre mis dos cosas, la misma diferencia existe entre tus dos cosas.
fuente
Task.Delay(1000).ContinueWith(() = {})
. En el segundo, es soloTask.Delay(1000)
. La diferencia es algo sutil, pero significativa.El primer método ni siquiera se compila.
Tiene que ser
async Task TestAsync() { await Task.Run(() => DoSomeWork()); }
Existe una gran diferencia conceptual entre estos dos. El primero es asincrónico, el segundo no lo es. Lea Async Performance: Understanding the Costs of Async y Await para obtener más información sobre los aspectos internos de
async
/await
.Generan código diferente.
.method private hidebysig instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74 2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73 79 6e 63 3e 64 5f 5f 31 00 00 ) .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x216c // Code size 62 (0x3e) .maxstack 2 .locals init ( [0] valuetype SOTestProject.Program/'<TestAsync>d__1', [1] class [mscorlib]System.Threading.Tasks.Task, [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder ) IL_0000: ldloca.s 0 IL_0002: ldarg.0 IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this' IL_0008: ldloca.s 0 IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create() IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder' IL_0014: ldloca.s 0 IL_0016: ldc.i4.m1 IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state' IL_001c: ldloca.s 0 IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder' IL_0023: stloc.2 IL_0024: ldloca.s 2 IL_0026: ldloca.s 0 IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&) IL_002d: ldloca.s 0 IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder' IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task() IL_0039: stloc.1 IL_003a: br.s IL_003c IL_003c: ldloc.1 IL_003d: ret } // end of method Program::TestAsync
y
.method private hidebysig instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed { // Method begins at RVA 0x21d8 // Code size 23 (0x17) .maxstack 2 .locals init ( [0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000 ) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'() IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int) IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>) IL_0012: stloc.0 IL_0013: br.s IL_0015 IL_0015: ldloc.0 IL_0016: ret } // end of method Program::TestAsync2
fuente
Los dos ejemplos no difieren. Cuando un método está marcado con la
async
palabra clave, el compilador genera una máquina de estado detrás de escena. Esto es lo que se encarga de reanudar las continuaciones una vez que se ha esperado una esperada.Por el contrario, cuando un método no está marcado con
async
, se pierde la capacidad deawait
esperar. (Es decir, dentro del método en sí; la persona que llama aún puede esperar el método). Sin embargo, al evitar laasync
palabra clave, ya no está generando la máquina de estado, que puede agregar un poco de sobrecarga (elevando locales a los campos de la máquina de estado, objetos adicionales al GC).En ejemplos como este, si puede evitar
async-await
y devolver un awaitable directamente, debe hacerlo para mejorar la eficiencia del método.Vea esta pregunta y esta respuesta que son muy similares a su pregunta y esta respuesta.
fuente