提问人:TX_ 提问时间:9/30/2013 最后编辑:Theodor ZouliasTX_ 更新时间:10/19/2023 访问量:70563
当您可以直接返回 Task<T> 时,为什么要使用 async 和 return await?
Why use async and return await, when you can return Task<T> directly?
问:
有没有这样的写法:
public async Task<SomeResult> DoSomethingAsync()
{
// Some synchronous code might or might not be here... //
return await DoAnotherThingAsync();
}
取而代之的是:
public Task<SomeResult> DoSomethingAsync()
{
// Some synchronous code might or might not be here... //
return DoAnotherThingAsync();
}
有意义吗?
当您可以从内部 DoAnotherThingAsync()
调用中直接返回 Task<T>
时,为什么要使用 return await
构造?
我在很多地方都看到了代码,我想我可能错过了一些东西。但据我了解,在这种情况下不使用 async/await 关键字并直接返回 Task 在功能上是等效的。为什么要增加附加层的额外开销?return await
await
答:
如果您不需要(即,您可以直接返回),则不要使用 .async
Task
async
在某些情况下,有一些情况很有用,例如,如果您有两个异步操作要执行:return await
var intermediate = await FirstAsync();
return await SecondAwait(intermediate);
有关性能的详细信息,请参阅 Stephen Toub 的 MSDN 文章和有关该主题的视频。async
更新:我写了一篇博文,详细介绍了这一点。
评论
await
return SecondAwait(intermediate);
return SecondAwait(intermediate);
return await
await
SecondAwait
您想要这样做的唯一原因是,如果前面的代码中有其他一些内容,或者您在返回结果之前以某种方式操作结果。发生这种情况的另一种方式是通过更改异常处理方式的方式。如果你没有做任何这些,那么你是对的,没有理由增加制作方法的开销。await
try/catch
async
评论
return await
async
async
async
await
await
await
async
async
Task<Type>
async
Type
Task<Type>
async
async
async
有一种偷偷摸摸的情况,即在正常方法和方法中的行为不同:当与块组合时(或者更一般地说,块中的任何)。return
return await
async
using
return await
try
请考虑以下两个版本的方法:
Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return foo.DoAnotherThingAsync();
}
}
async Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return await foo.DoAnotherThingAsync();
}
}
第一个方法将在方法返回后立即返回对象,这可能在实际完成之前很久。这意味着第一个版本可能有问题(因为处理得太早),而第二个版本可以正常工作。Dispose()
Foo
DoAnotherThingAsync()
Foo
评论
foo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
Dispose()
void
return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });
{ var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }
Foo
Task
您可能需要等待结果的另一种情况是:
async Task<IFoo> GetIFooAsync()
{
return await GetFooAsync();
}
async Task<Foo> GetFooAsync()
{
var foo = await CreateFooAsync();
await foo.InitializeAsync();
return foo;
}
在这种情况下,必须等待结果,因为两种方法之间的类型不同,并且不能直接分配给 。但是,如果您等待结果,它就会变成可直接分配给 .然后,异步方法只是在内部重新打包结果,然后就可以了。GetIFooAsync()
GetFooAsync
T
Task<Foo>
Task<IFoo>
Foo
IFoo
Task<IFoo>
评论
Task<>
使原本简单的“thunk”方法异步会在内存中创建一个异步状态机,而非异步状态机则不会。虽然这通常可以指向人们使用非异步版本,因为它更有效率(这是真的),但这也意味着在挂起的情况下,你没有证据表明该方法涉及“返回/延续堆栈”,这有时会使理解挂起变得更加困难。
所以,是的,当 perf 不重要(通常不重要)时,我会在所有这些 thunk 方法上抛出异步,这样我就有了异步状态机来帮助我以后诊断挂起,并帮助确保如果这些 thunk 方法随着时间的推移而发展,它们肯定会返回出错的任务而不是抛出。
这也让我感到困惑,我觉得前面的答案忽略了你的实际问题:
当您可以从内部 DoAnotherThingAsync() 调用直接返回 Task 时,为什么要使用 return await 构造?
有时你实际上想要一个 ,但大多数时候你实际上想要一个 的实例,即任务的结果。Task<SomeType>
SomeType
从您的代码:
async Task<SomeResult> DoSomethingAsync()
{
using (var foo = new Foo())
{
return await foo.DoAnotherThingAsync();
}
}
不熟悉语法的人(例如我)可能会认为此方法应该返回 ,但由于它标有 ,这意味着它的实际返回类型是 。
如果你只使用 ,你会返回一个 Task,它不会编译。正确的方法是返回任务的结果,因此.Task<SomeResult>
async
SomeResult
return foo.DoAnotherThingAsync()
return await
评论
var task = DoSomethingAsync();
T
async/await
Task task = DoSomethingAsync()
Something something = await DoSomethingAsync()
await
Task task = DoSomethingAsync(); Something something = await task;
如果您不使用 return await,则可能会在调试时或在异常日志中打印堆栈跟踪时破坏堆栈跟踪。
当您返回任务时,该方法实现了其目的,并且它不在调用堆栈中。
使用时,会将其保留在调用堆栈中。return await
例如:
使用 await 时调用堆栈: A 等待 B 的任务 => B 等待 C 的任务
不使用 await 时调用堆栈: A 正在等待 C 的任务,而 B 已返回该任务。
评论
您可能想要的另一个原因:语法可以避免在 和 类型之间遇到不匹配。例如,即使 SubTask 方法返回但其调用方返回 ,下面的代码仍然有效。return await
await
Task<T>
ValueTask<T>
Task<T>
ValueTask<T>
async Task<T> SubTask()
{
...
}
async ValueTask<T> DoSomething()
{
await UnimportantTask();
return await SubTask();
}
如果在行上跳过 await,则会收到编译器错误 CS0029:DoSomething()
无法将类型“System.Threading.Tasks.Task<BlaBla>”隐式转换为“System.Threading.Tasks.ValueTask<BlaBla>”。
如果您尝试显式类型转换 CS0030,您也会得到它。
顺便说一下,这是 .NET Framework。我完全可以预见到一条评论说“这在 .NET hypothetical_version 中已修复”,我还没有测试过它。:)
评论
非 await 方法的另一个问题是有时您无法隐式转换返回类型,尤其是:Task<IEnumerable<T>>
async Task<List<string>> GetListAsync(string foo) => new();
// This method works
async Task<IEnumerable<string>> GetMyList() => await GetListAsync("myFoo");
// This won't work
Task<IEnumerable<string>> GetMyListNoAsync() => GetListAsync("myFoo");
错误:
无法将类型“System.Threading.Tasks.Task<System.Collections.Generic.List>”隐式转换为“System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable>”
这将阻止创建将在编译时在后台创建的任务状态机。但根据 David 的说法,出于以下原因,您应该更喜欢 async/await 而不是直接返回 Task:
- 异步和同步异常被规范化为始终是异步的。
- 代码更易于修改(例如,考虑添加 using)。
- 异步方法的诊断更容易(调试挂起等)。
- 抛出的异常将自动包装在返回的 Task 中,而不是用实际的异常让调用者感到惊讶。
- 异步本地变量不会从异步方法中泄漏出来。如果在非异步方法中设置了 async local,它将从该调用中“泄漏”出来。
💡注意:使用异步状态时存在性能注意事项 机器直接返回任务。它总是更快 直接返回任务,因为它做的工作较少,但你最终会 改变行为并可能失去一些好处 异步状态机。
如果你真的想打破异步,你应该知道异步是病毒式的:
一旦你异步,你的所有调用方都应该是异步的,因为努力 除非整个调用堆栈是异步的,否则异步就等于什么都没有。在 在许多情况下,部分异步可能比完全异步更糟糕 同步。因此,最好全力以赴,把一切都做好 异步。
评论