tag ejemplos ejemplo await async c# asynchronous async-await task-parallel-library task

ejemplos - tag c#



Forma correcta de implementar métodos que devuelven la Tarea<T> (2)

Entonces, ¿cuál es el enfoque correcto?

Ninguno.

Si tiene que hacer un trabajo síncrono , entonces la API debería ser síncrona :

public object Foo() { // some heavy synchronous stuff. return new object(); }

Si el método de llamada puede bloquear su subproceso (es decir, es una llamada de ASP.NET o se está ejecutando en un subproceso de agrupación de subprocesos), entonces simplemente lo llama directamente:

var result = Foo();

Y si el subproceso que llama no puede bloquear su subproceso (es decir, se está ejecutando en el subproceso de la interfaz de usuario), entonces puede ejecutar Foo en el grupo de subprocesos:

var result = await Task.Run(() => Foo());

Como describo en mi blog, Task.Run debe usarse para invocación, no para implementación .

Ejemplo del mundo real

(que es un escenario completamente diferente)

¿Qué implementación de Tarea GetUserByNameAsync (string userName) sería correcta?

Cualquiera de las dos es aceptable. El que tiene async y await tiene una sobrecarga adicional, pero no se notará en el tiempo de ejecución (suponiendo que la cosa que está await en realidad hace E / S, lo cual es cierto en el caso general).

Tenga en cuenta que si hay otro código en el método, entonces es mejor el de async y await . Este es un error común:

Task<string> MyFuncAsync() { using (var client = new HttpClient()) return client.GetStringAsync("http://www.example.com/"); }

En este caso, el HttpClient se elimina antes de que finalice la tarea.

Otra cosa a tener en cuenta es que las excepciones antes de devolver la tarea se producen de manera diferente:

Task<string> MyFuncAsync(int id) { ... // Something that throws InvalidOperationException return OtherFuncAsync(); }

Dado que no hay async , la excepción no se coloca en la tarea devuelta; Se lanza directamente. Esto puede confundir el código de llamada si hace algo más complejo que simplemente await la tarea:

var task1 = MyFuncAsync(1); // Exception is thrown here. var task2 = MyFuncAsync(2); ... try { await Task.WhenAll(task1, task2); } catch (InvalidOperationException) { // Exception is not caught here. It was thrown at the first line. }

Para simplificar, imaginemos que tenemos un método que debe devolver un objeto mientras realiza una operación pesada. Hay dos maneras de implementar:

public Task<object> Foo() { return Task.Run(() => { // some heavy synchronous stuff. return new object(); } }

Y

public async Task<object> Foo() { return await Task.Run(() => { // some heavy stuff return new object(); } }

Después de examinar la IL generada, se generan dos cosas completamente diferentes:

.method public hidebysig instance class [mscorlib]System.Threading.Tasks.Task`1<object> Foo () cil managed { // Method begins at RVA 0x2050 // Code size 42 (0x2a) .maxstack 2 .locals init ( [0] class [mscorlib]System.Threading.Tasks.Task`1<object> ) IL_0000: nop IL_0001: ldsfld class [mscorlib]System.Func`1<object> AsyncTest.Class1/''<>c''::''<>9__0_0'' IL_0006: dup IL_0007: brtrue.s IL_0020 IL_0009: pop IL_000a: ldsfld class AsyncTest.Class1/''<>c'' AsyncTest.Class1/''<>c''::''<>9'' IL_000f: ldftn instance object AsyncTest.Class1/''<>c''::''<Foo>b__0_0''() IL_0015: newobj instance void class [mscorlib]System.Func`1<object>::.ctor(object, native int) IL_001a: dup IL_001b: stsfld class [mscorlib]System.Func`1<object> AsyncTest.Class1/''<>c''::''<>9__0_0'' IL_0020: call class [mscorlib]System.Threading.Tasks.Task`1<!!0> [mscorlib]System.Threading.Tasks.Task::Run<object>(class [mscorlib]System.Func`1<!!0>) IL_0025: stloc.0 IL_0026: br.s IL_0028 IL_0028: ldloc.0 IL_0029: ret }

Y

.method public hidebysig instance class [mscorlib]System.Threading.Tasks.Task`1<object> Foo () cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 1a 41 73 79 6e 63 54 65 73 74 2e 43 6c 61 73 73 31 2b 3c 42 61 72 3e 64 5f 5f 31 00 00 ) .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2088 // Code size 59 (0x3b) .maxstack 2 .locals init ( [0] class AsyncTest.Class1/''<Foo>d__1'', [1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> ) IL_0000: newobj instance void AsyncTest.Class1/''<Foo>d__1''::.ctor() IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: ldarg.0 IL_0008: stfld class AsyncTest.Class1 AsyncTest.Class1/''<Foo>d__1''::''<>4__this'' IL_000d: ldloc.0 IL_000e: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::Create() IL_0013: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> AsyncTest.Class1/''<Foo>d__1''::''<>t__builder'' IL_0018: ldloc.0 IL_0019: ldc.i4.m1 IL_001a: stfld int32 AsyncTest.Class1/''<Foo>d__1''::''<>1__state'' IL_001f: ldloc.0 IL_0020: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> AsyncTest.Class1/''<Foo>d__1''::''<>t__builder'' IL_0025: stloc.1 IL_0026: ldloca.s 1 IL_0028: ldloca.s 0 IL_002a: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::Start<class AsyncTest.Class1/''<Foo>d__1''>(!!0&) IL_002f: ldloc.0 IL_0030: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> AsyncTest.Class1/''<Foo>d__1''::''<>t__builder'' IL_0035: call instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::get_Task() IL_003a: ret }

Como se puede ver en el primer caso, la lógica es sencilla, se crea la función lambda y luego se genera una llamada a la Task.Run Se Task.Run la Task.Run y se devuelve el resultado. En la segunda instancia de ejemplo de AsyncTaskMethodBuilder se crea y luego la tarea se genera y se devuelve. Como siempre he esperado que se llame al método foo como await Foo() en un nivel superior, siempre he usado el primer ejemplo. Sin embargo, veo este último más a menudo. Entonces, ¿cuál es el enfoque correcto? ¿Qué pros y contras tiene cada uno?

Ejemplo del mundo real

Digamos que tenemos UserStore que tiene el método Task<User> GetUserByNameAsync(string userName) que se usa dentro del controlador web api como:

public async Task<IHttpActionResult> FindUser(string userName) { var user = await _userStore.GetUserByNameAsync(userName); if (user == null) { return NotFound(); } return Ok(user); }

¿Qué implementación de la Task<User> GetUserByNameAsync(string userName) sería correcta?

public Task<User> GetUserByNameAsync(string userName) { return _dbContext.Users.FirstOrDefaultAsync(user => user.UserName == userName); }

o

public async Task<User> GetUserNameAsync(string userName) { return await _dbContext.Users.FirstOrDefaultAsync(user => user.UserName == username); }


Como puede ver en el IL, async/await crea una máquina de estado (y una Task adicional) incluso en el caso de llamadas de cola asíncronas triviales, es decir

return await Task.Run(...);

Esto conduce a una degradación del rendimiento debido a instrucciones y asignaciones adicionales. Entonces, la regla de oro es: si su método termina con await ... o return await ... , y es la única y única instrucción de await , generalmente es seguro eliminar la palabra clave async y devolver directamente la Task que estaba voy a esperar

Una consecuencia potencialmente no deseada de hacer esto es que si se lanza una excepción dentro de la Task devuelta, el método externo no aparecerá en el seguimiento de la pila.

Hay un gotcha oculto en el return await ... caso también. Si el observador no está configurado explícitamente para no continuar en el contexto capturado a través de ConfigureAwait(false) , entonces la Task externa (la creada para usted por la máquina de estado asíncrona) no puede pasar al estado completo hasta la devolución final al SynchronizationContext (capturado solo antes de la await ) ha terminado. Esto no tiene un propósito real, pero aún puede resultar en un punto muerto si bloquea en la tarea externa por alguna razón ( aquí hay una explicación detallada de lo que sucede en tal caso).