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

时间:2016-03-25 07:44:30

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

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

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

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

2 个答案:

答案 0 :(得分:12)

  

那么哪种方法是正确的?

都不是。

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

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

    return new object();
}

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

var result = Foo();

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

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

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

  

真实世界的例子

(这是完全不同的情况)

  

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

任何一个都可以接受。 asyncawait的开销有一些额外的开销,但在运行时却不会引人注意(假设您await实际上做了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,因此在返回的任务中放置 not 例外;它被直接抛出。如果调用代码执行比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 :(得分:11)

从IL可以看出,async/await创建了一个状态机(以及一个额外的Task),即使是在无关紧要的异步尾调用的情况下,即

return await Task.Run(...);

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

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

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