Правильный способ реализации методов, возвращающих Task <T>
Для простоты предположим, что у нас есть метод, который должен возвращать объект при выполнении тяжелой операции. Существует два способа реализации:
public Task<object> Foo()
{
return Task.Run(() =>
{
// some heavy synchronous stuff.
return new object();
}
}
и
public async Task<object> Foo()
{
return await Task.Run(() =>
{
// some heavy stuff
return new object();
}
}
После изучения сгенерированного ИЛ есть две совершенно разные вещи:
.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
}
и
.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
}
Как вы можете видеть в первом случае логика проста, создается лямбда-функция, а затем вызывается вызов Task.Run
и возвращается результат. Во втором примере создается экземпляр AsyncTaskMethodBuilder
, и тогда задача фактически строится и возвращается. Поскольку я всегда ожидал, что метод foo будет называться как await Foo()
на каком-то более высоком уровне, я всегда использовал первый пример. Тем не менее, я вижу последнее чаще. Итак, какой подход правильный? Какие плюсы и минусы у каждого есть?
Пример реального мира
Скажем, мы имеем UserStore
, у которого есть метод Task<User> GetUserByNameAsync(string userName)
, который используется внутри веб-контроллера api, например:
public async Task<IHttpActionResult> FindUser(string userName)
{
var user = await _userStore.GetUserByNameAsync(userName);
if (user == null)
{
return NotFound();
}
return Ok(user);
}
Какая реализация Task<User> GetUserByNameAsync(string userName)
была бы правильной?
public Task<User> GetUserByNameAsync(string userName)
{
return _dbContext.Users.FirstOrDefaultAsync(user => user.UserName == userName);
}
или
public async Task<User> GetUserNameAsync(string userName)
{
return await _dbContext.Users.FirstOrDefaultAsync(user => user.UserName == username);
}
Ответы
Ответ 1
Как вы можете видеть из IL, async/await
создает конечный автомат (и дополнительный Task
) даже в случае тривиальных хвостовых вызовов aync, т.е.
return await Task.Run(...);
Это приводит к ухудшению производительности за счет дополнительных инструкций и распределений. Итак, эмпирическое правило: если ваш метод заканчивается на await ...
или return await ...
, и это оператор один и только await
, тогда в целом безопасно удалять ключевое слово async
и прямо верните Task
, который вы ожидаете.
Одно потенциально непреднамеренное следствие этого заключается в том, что если исключение выбрано внутри возвращенного Task
, внешний метод не будет отображаться в трассировке стека.
В случае с return await ...
есть скрытый заголовок. Если awaiter явно не сконфигурирован, чтобы не продолжать в захваченном контексте через ConfigureAwait(false)
, тогда внешний Task
(тот, который был создан для машины состояния async) не может перейти в завершенное состояние до окончательной обратной передачи в SynchronizationContext
(захвачен непосредственно перед await
). Это не имеет никакой реальной цели, но все равно может привести к тупиковой ситуации, если по какой-то причине вы заблокируете внешнюю задачу (здесь подробное объяснение о том, что происходит в этом случае).
Ответ 2
Итак, какой подход правильный?
Ни.
Если у вас есть синхронная работа, тогда API должен быть синхронным:
public object Foo()
{
// some heavy synchronous stuff.
return new object();
}
Если вызывающий метод может блокировать свой поток (т.е. он представляет собой вызов ASP.NET или работает в потоке пула потоков), он просто вызывает его напрямую:
var result = Foo();
И если вызывающий поток не может заблокировать поток (т.е. он работает в потоке пользовательского интерфейса), тогда он может запустить Foo
в пуле потоков:
var result = await Task.Run(() => Foo());
Как я описываю в своем блоге, Task.Run
должен использоваться для вызова, а не для реализации.
Пример реального мира
(это совершенно другой сценарий)
Какая реализация Task GetUserByNameAsync (string userName) будет правильной?
Любой из них является приемлемым. Тот, у которого есть async
и await
, имеет некоторые дополнительные накладные расходы, но он не будет заметен во время выполнения (при условии, что вы await
ing действительно делают I/O, что истинно в общем случае).
Обратите внимание, что если в методе есть другой код, то лучше с async
и await
. Это распространенная ошибка:
Task<string> MyFuncAsync()
{
using (var client = new HttpClient())
return client.GetStringAsync("http://www.example.com/");
}
В этом случае HttpClient
устанавливается до завершения задачи.
Еще одна вещь, которую следует отметить, заключается в том, что исключения перед возвратом задачи различаются по-разному:
Task<string> MyFuncAsync(int id)
{
... // Something that throws InvalidOperationException
return OtherFuncAsync();
}
Поскольку нет async
, исключение не помещается в возвращаемую задачу; он бросается напрямую. Это может запутать вызывающий код, если он делает что-то более сложное, чем просто await
выполнить задачу:
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.
}