正确的方法来实现返回Task <T>的方法

Ler*_*eri 18 c# asynchronous task task-parallel-library async-await

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

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

        return new object();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在检查生成的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
}
Run Code Online (Sandbox Code Playgroud)

.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
}
Run Code Online (Sandbox Code Playgroud)

正如您在第一种情况中所看到的那样,逻辑很简单,创建了lambda函数,然后Task.Run生成调用并返回结果.在AsyncTaskMethodBuilder创建的第二个示例实例中,然后实际构建并返回任务.因为我总是希望foo方法await Foo()在某个更高级别被调用,所以我总是使用第一个例子.但是,我经常看到后者.那么哪种方法是正确的?每个人有什么利弊?


现实世界的例子

假设我们有UserStore一个Task<User> 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);
}
Run Code Online (Sandbox Code Playgroud)

哪个实施Task<User> GetUserByNameAsync(string userName)是正确的?

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

要么

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

Ste*_*ary 12

那么哪种方法是正确的?

都不是.

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

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

    return new object();
}
Run Code Online (Sandbox Code Playgroud)

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

var result = Foo();
Run Code Online (Sandbox Code Playgroud)

如果调用线程无法阻止它的线程(即,它在UI线程上运行),那么它可以Foo在线程池上运行:

var result = await Task.Run(() => Foo());
Run Code Online (Sandbox Code Playgroud)

正如我在博客中描述的那样,Task.Run应该用于调用,而不是实现.


真实世界的例子

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

任务GetUserByNameAsync(字符串userName)的哪个实现是正确的?

任何一个都是可以接受的.有一个async并且await有一些额外开销的那个,但它在运行时不会引人注意(假设你await实际上是I/O的东西,这在一般情况下是正确的).

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

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

在这种情况下,HttpClient在任务完成之前处理.

另外需要注意的是,返回任务之前的异常会以不同方式抛出:

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

由于没有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.
}
Run Code Online (Sandbox Code Playgroud)


Kir*_*kiy 11

正如您在IL中看到的那样,即使在非平凡的异步尾调用的情况下,也会async/await创建一个状态机(以及一个额外的Task),即

return await Task.Run(...);
Run Code Online (Sandbox Code Playgroud)

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

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

return await ...尽管如此,案件中还有一个隐藏的问题.如果未明确将awaiter配置为继续捕获的上下文ConfigureAwait(false),则外部Task(由异步状态机为您创建的那个)无法转换到完成状态,直到最后一次回发SynchronizationContext(在之前捕获await)完成.这没有任何实际意义,但如果由于某种原因阻止外部任务,仍然可能导致死锁(这里详细解释了在这种情况下会发生什么).