实现 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?
两者皆可。 async
和 await
有一些额外的开销,但在 运行 时不会引人注意(假设你正在 await
ing 的东西确实 I/O,这在一般情况下是正确的)。
请注意,如果方法中还有其他代码,则带有 async
和 await
的代码更好。这是一个常见的错误:
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.
}
为简单起见,假设我们有一个方法应该 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?
两者皆可。 async
和 await
有一些额外的开销,但在 运行 时不会引人注意(假设你正在 await
ing 的东西确实 I/O,这在一般情况下是正确的)。
请注意,如果方法中还有其他代码,则带有 async
和 await
的代码更好。这是一个常见的错误:
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.
}