begin/end 方法的核心契约是调用 beginxxx 必须配对调用 endxxx,endxxx 是唯一能获取返回值、抛出异常、释放资源的入口;漏掉会导致异常丢失、句柄泄漏或连接池耗尽。

Begin/End 方法的核心契约是什么
调用 BeginXxx 必须配对调用 EndXxx,哪怕你用轮询或回调,EndXxx 是唯一能获取返回值、抛出异常、释放底层资源的入口。漏掉 EndXxx 不仅会丢失异常(比如网络超时被静默吞掉),还可能造成句柄泄漏或连接池耗尽。
常见错误现象:BeginRead 后没调 EndRead,后续读操作开始缓慢甚至卡死;BeginInvoke 后忽略 EndInvoke,委托内部抛出的异常永远不浮现。
-
BeginXxx返回IAsyncResult,它只是个“凭证”,不包含结果也不保证执行完成 -
EndXxx必须传入对应的IAsyncResult,且只能调一次 —— 重复调用会抛InvalidOperationException - 即使
BeginXxx立即同步完成(如缓冲区有现成数据),EndXxx仍需调用才能取值
三种等待方式怎么选:轮询 / WaitHandle / 回调
不是所有场景都适合回调,尤其在 UI 线程或受限上下文里,回调可能引发死锁或上下文切换开销。选择依据是「谁负责触发后续逻辑」和「能否接受阻塞」。
轮询(IsCompleted)极少用,纯浪费 CPU;WaitHandle 适合需要超时控制或与其他信号(如 ManualResetEvent)组合的场景;回调(AsyncCallback)最常见,但要注意捕获异常必须在 EndXxx 里做,不能只靠 try/catch 包裹回调体。
- 用
asyncResult.AsyncWaitHandle.WaitOne(3000)等待 3 秒,超时后可主动取消或重试 - 回调函数中第一件事就是调
EndXxx,否则异常不会冒泡,try/catch包住整个回调体无效 - UI 线程调
BeginXxx+ 回调,回调默认不在 UI 线程执行 —— 更新控件前需Control.Invoke或Dispatcher.BeginInvoke
AsyncCallback 里怎么安全处理异常和结果
APM 的异常机制很隐蔽:BeginXxx 几乎不抛异常(除参数校验失败),真正异常全压在 EndXxx 里。回调函数若没调 EndXxx,异常就丢了。
void ReadCallback(IAsyncResult ar) {
try {
// ✅ 正确:立刻 EndRead 获取结果和潜在异常
int bytesRead = stream.EndRead(ar);
ProcessData(buffer, bytesRead);
}
catch (IOException ex) {
// ❌ 错误:这里永远不会进,除非 EndRead 外部再抛新异常
LogError(ex);
}
}
- 所有输入参数(如 buffer、offset)必须和
BeginRead传的一致,否则EndRead可能抛ArgumentException - 回调中不要直接访问
ar.AsyncState以外的闭包变量,多线程下可能被修改(尤其 lambda 捕获循环变量) - 如果
BeginXxx传了自定义 state 对象,务必检查ar.AsyncState != null再转型,避免NullReferenceException
为什么现在基本不该新写 APM 代码
APM 在 .NET Framework 时代是主流,但它的嵌套深、易出错、调试难,已被 async/await 全面替代。.NET Core / .NET 5+ 中多数 API 已移除 Begin/End,只保留 Task-based 方法。
唯一还绕不开 APM 的场景是维护老代码,或对接极少数只提供 APM 接口的类库(如某些 COM 组件封装)。此时注意:别试图把 APM 包装成 Task 后再 await —— Task.Factory.FromAsync 虽支持,但增加了不必要的调度开销,且掩盖了原始错误语义。
- 新项目一律用
Stream.ReadAsync、WebClient.DownloadStringTaskAsync等 Task 方法 - 老代码迁移时,优先替换为
Task.Run(() => { ... }).Unwrap()这类显式包装,比FromAsync更易调试 -
IAsyncResult的CompletedSynchronously属性常被忽略 —— 它表示是否同步完成,影响你是否要手动调度后续逻辑










