从 BeginInvoke
到 async/await
:C# 异步模型的三次关键切换
早期 .NET 2.0 的异步编程靠手写
IAsyncResult模式,代码嵌套深、错误处理难、调试困难。这不是“写法偏好”问题,而是运行时根本没提供上下文延续能力——
EndInvoke返回后,原始栈帧早已销毁。
Task
类型出现前的三大模式及其崩溃点
.NET 3.5–4.0 间并存三种异步写法,但都绕不开状态机手动维护:
BeginXxx/EndXxx(APM):必须配对调用,
EndXxx被遗漏会导致线程挂起或资源泄漏
Event-based Async Pattern(EAP):如
WebClient.DownloadStringAsync,事件回调中无法用
return传递结果,异常只能靠
RunWorkerCompletedEventArgs.Error传递 手动创建
Thread或
ThreadPool.QueueUserWorkItem:完全脱离调度器控制,
SynchronizationContext无法自动捕获,UI 线程更新必崩
async/await
不是语法糖,而是编译器+运行时协同重构状态机
真正改变游戏规则的是 C# 5.0 + .NET 4.5 的组合:
async方法被编译为
Task-返回的状态机类,而
await表达式会触发
GetAwaiter().OnCompleted()注册回调,并在恢复时自动切回原
SynchronizationContext(如 WinForms 的
Control.InvokeRequired场景)。
这意味着:
不再需要显式ContinueWith链式调用,嵌套深度归零
try/catch可直接捕获异步操作中的异常,无需拆解
AggregateException
ConfigureAwait(false)成为性能关键开关:后台服务中不加它,每次 await 后都尝试切回原始上下文,徒增调度开销
public async Task<string> FetchDataAsync()
{
// 下面这行 await 完成后,线程可能已切换
// 但 this.InvokeRequired 仍能正确判断是否需跨线程
var result = await httpClient.GetStringAsync("https://api.example.com");
return result.ToUpper();
}现代项目里还可能踩到的兼容性暗坑
即便用着 C# 10,只要目标框架是
net472或更低,
ValueTask就无法享受结构体优化;而
net6.0+中
async方法若返回
Task却未真正异步(比如直接
return Task.FromResult(...)),就会多分配一个状态机对象。
更隐蔽的是:ASP.NET Core 2.1+ 默认禁用
HttpContext.Capture,导致在中间件中
await后访问
HttpContext.Request可能抛出
ObjectDisposedException——这不是代码写错,而是运行时生命周期管理逻辑变了。
