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).