C# 中 await 应该传播到哪里,可以用 async void 结束调用链吗?
结论:让调用链到达能够承接异步操作的入口
通常应让异步调用沿调用链向上传递,直到 async Task Main、框架支持的异步回调或事件处理器等入口。不要为了停止传播,就把普通业务方法改成 async void。
方法不一定都需要 async。需要等待异步结果并继续处理时,通常使用 async/await;如果只是转交任务,可以直接返回 Task;没有异步依赖的同步方法可以保持同步。
为什么调用方也需要 await?
GetNumberAsync() 返回的 Task<int> 代表一个最终会产生 int 结果的操作。如果调用方要用这个数字继续计算,就需要等待任务完成:
async Task AnotherMethodAsync()
{
var number = await GetNumberAsync();
var result = number + 5;
}
这里的 await 表明,代码要先得到 number,再计算 result。如果任务尚未完成,await 可以挂起当前方法,将控制权交还给调用方,而不必阻塞当前线程。
AnotherMethodAsync() 返回的 Task 让上一层调用方知道整个操作何时完成、是否失败。如果上一层依赖该操作完成,也应该等待它。
原示例中的 result 只是局部变量。如果调用方也需要这个计算结果,应将返回类型改为 Task<int>,并使用 return result;。
返回 Task 的方法不一定需要 async
如果一个方法只是把另一个方法的任务交给调用方,等待完成后也没有其他处理逻辑,就可以直接返回任务。在这一前提下,原来的包装方法可以写成:
Task<int> GetNumberAsync()
{
return GettingNumberFromHttpClient();
}
调用方仍然可以使用:
var number = await GetNumberAsync();
async 允许方法体使用 await;返回类型 Task<int> 告诉调用方,该方法提供一个异步结果。两者不一定要同时出现。
不过,不能一概把 return await 改为直接返回任务。如果方法需要用 try/catch 捕获异步失败,或让 using 中的资源保留到操作完成,通常仍需在相应作用域内使用 await。
调用链最终在哪里结束?
在支持异步入口的 C# 控制台程序中,可以一直等待到 Main:
static async Task Main()
{
await AnotherMethodAsync();
}
这里假设相关方法可从该静态入口调用。运行时会等待 Main 返回的任务完成,应用代码不需要再增加一层调用者。
在 Web 应用中,支持异步处理的框架可以接收请求处理方法返回的 Task,并跟踪请求处理的完成状态。具体签名需要遵循所用框架的约定。
调用链最终会到达由运行时或框架承接的边界,不会无限延伸。
为什么 async void 不适合普通业务方法?
把方法改为 async void 后,方法内部仍会执行异步操作,但调用方拿不到代表其完成状态的任务,因此无法:
- 使用
await等待它完成。 - 将它交给
Task.WhenAll等方法组合执行。 - 通过等待返回的任务来观察失败。
例如,下面的写法不能可靠地捕获 async void 方法体中抛出的异常:
try
{
AnotherMethod(); // async void
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
这类异常会通过调用时的同步上下文等机制传播,不会作为可等待任务的失败交给调用方。如果没有相应处理,可能导致应用终止。
使用返回 Task 的方法时,可以围绕 await 正常处理异常:
try
{
await AnotherMethodAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
什么时候可以用 async void?
典型场景是返回类型必须为 void 的事件处理器,例如某些 UI 框架的按钮点击事件。事件处理器可以作为最外层边界,内部调用返回 Task 的业务方法,并在边界处理异常:
async void Button_Click(object sender, EventArgs e)
{
try
{
await AnotherMethodAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
事件处理器的具体签名应以框架要求为准,异常处理方式也应按应用需要调整。
不要仅为消除 await 就改用 .Wait()、.Result 或 .GetAwaiter().GetResult()。这些写法会同步阻塞线程,在某些同步上下文中还可能造成死锁。确实只能提供同步接口时,需要单独评估这个边界。
备注:内容仅供参考。
