实现 return Task<T> 方法的正确方法

Proper way to implement methods that return Task<T>

为简单起见,假设我们有一个方法应该 return 一个对象,同时进行一些繁重的操作。有两种实现方式:

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();
    }
}

检查生成的 IL 后,生成了两个完全不同的东西:

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

正如您在第一种情况中看到的那样,逻辑很简单,创建了 lambda 函数,然后生成了对 Task.Run 的调用,结果是 returned。在第二个示例中,创建了 AsyncTaskMethodBuilder 实例,然后实际构建并 return 了任务。由于我一直希望 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);
}

正如您从 IL 中看到的那样,async/await 创建了一个状态机(以及一个额外的 Task),即使是在普通的异步尾调用的情况下,即

return await Task.Run(...);

由于额外的指令和分配,这会导致性能下降。所以经验法则是:如果你的方法以 await ...return await ... 结尾,并且它是 one and only await 语句,那么它是通常 安全删除 async 关键字并直接 return 您要等待的 Task

这样做的一个潜在意外后果是,如果在 returned Task 内部抛出异常,则外部方法将不会出现在堆栈跟踪中。

不过 return await ... 案例中也有一个隐藏的问题。如果未明确配置等待者 而不是 以通过 ConfigureAwait(false) 继续捕获上下文,则外部 Task (由异步状态机为您创建的那个)在 SynchronizationContext 的最终回发(在 await 之前捕获)完成之前无法转换为完成状态。这没有实际用途,但如果您出于某种原因阻止外部任务(here's a detailed explanation 在这种情况下发生的情况),仍然会导致死锁。

So which approach is correct?

都没有。

如果您有同步工作要做,那么API应该是同步的:

public object Foo()
{
    // some heavy synchronous stuff.

    return new object();
}

如果调用方法可以阻塞它的线程(即,它是一个 ASP.NET 调用,或者它在线程池线程上 运行ning),那么它直接调用它:

var result = Foo();

并且如果调用线程不能阻塞它的线程(即它在 UI 线程上 运行ning),那么它可以 运行 Foo线程池:

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

正如我在我的博客中所描述的那样,Task.Run should be used for invocation, not implementation


Real World Example

(完全不同的场景)

Which implementation of Task GetUserByNameAsync(string userName) would be correct?

两者皆可。 asyncawait 有一些额外的开销,但在 运行 时不会引人注意(假设你正在 awaiting 的东西确实 I/O,这在一般情况下是正确的)。

请注意,如果方法中还有其他代码,则带有 asyncawait 的代码更好。这是一个常见的错误:

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

在这种情况下,HttpClient 在任务完成之前被释放。

另一件需要注意的事情是 before 返回任务的异常抛出方式不同:

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