【问题标题】:Proper way to implement methods that return Task<T>实现返回 Task<T> 的方法的正确方法
【发布时间】:2016-03-25 07:44:30
【问题描述】:

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

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 的调用并返回结果。在AsyncTaskMethodBuilder 的第二个示例实例中创建,然后实际构建并返回任务。由于我一直希望 foo 方法在更高级别被称为await Foo(),所以我一直使用第一个示例。但是,我经常看到后者。那么哪种方法是正确的呢?各有什么优缺点?


现实世界的例子

假设我们有 UserStore,它有方法 Task&lt;User&gt; GetUserByNameAsync(string userName),在 web api 控制器中使用,例如:

public async Task<IHttpActionResult> FindUser(string userName)
{
    var user = await _userStore.GetUserByNameAsync(userName);

    if (user == null)
    {
        return NotFound();
    }

    return Ok(user);
}

Task&lt;User&gt; 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);
}

【问题讨论】:

标签: c# asynchronous async-await task-parallel-library task


【解决方案1】:

那么哪种方法是正确的呢?

都没有。

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

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

    return new object();
}

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

var result = Foo();

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

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

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


现实世界的例子

(这是一个完全不同的场景)

Task GetUserByNameAsync(string userName) 的哪个实现是正确的?

任何一个都可以接受。带有asyncawait 的那个有一些额外的开销,但它在运行时不会很明显(假设你awaiting 的东西实际上进行了I/O,这在一般情况下是正确的)。

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

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

【讨论】:

  • +1。好吧,对于您回答的第一部分,Task.Run 仅用于演示。对于第二部分,如果一个应用程序使用异常来控制应用程序流,那么你是完全正确的。但是,我让我的异常成为异常,即在我当前的架构中,如果抛出异常意味着发生了严重错误,所以我不会像你展示的那样处理它们。
  • using不使用数据库和http客户端等类?即使您无法处理异常,您仍然应该进行适当的清理@Leri。这个答案的第二部分很重要。
  • @BenjaminGruenbaum 对于网络开发没有。我让 di scope 为我做这件事。
【解决方案2】:

从 IL 中可以看出,async/await 会创建一个状态机(以及一个额外的 Task),即使在微不足道的异步尾调用的情况下也是如此,即

return await Task.Run(...);

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

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

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

【讨论】:

  • 谢谢。很好解释的答案。恕我直言,Stacktrace 永远不值得表演。除此之外,人们还有什么理由使用微不足道的异步尾调用?
  • @Leri,我想不出任何其他理由这样做,而且我在 Roslyn 团队讨论中没有看到任何关于使编译器优化向前发展的内容:github.com/dotnet/roslyn/issues/1981跨度>
  • 我明白了。好吧,从这一点来看,我认为我的问题的答案很明显。谢谢。
猜你喜欢
  • 2015-01-01
  • 1970-01-01
  • 2016-09-23
  • 1970-01-01
  • 2016-10-05
  • 1970-01-01
  • 2014-08-14
  • 1970-01-01
  • 2017-06-04
相关资源
最近更新 更多