C# async、取消与资源所有权
前置:委托与异常、延迟执行。理解文件和输入输出不要求先学习 ASP.NET;本课程使用标准库,不引入网络服务。
async 状态机与 Task
Task<T> 表示一个异步操作的完成状态和结果,不代表独占线程。调用 async 方法会同步执行到第一个尚未完成的 await;await 记录后续步骤后将控制权交回调用者,完成时再继续。异常通常存入返回的 Task,await 时重新观察到;丢弃 Task 会使失败和退出时机失去管理。除事件处理器外使用 Task 返回类型,不用 async void 让调用者无法等待。
I/O 有异步 API 时直接 await;CPU 密集计算不会因为加 async 就释放CPU,可在确有必要的桌面响应或受控后台任务中使用 Task.Run,但要限制并行工作量。启动一万个任务再 WhenAll 不等于设置并发上限。同步 .Result/.Wait() 会阻塞线程,在有同步上下文的环境还可能死锁;应沿调用链 await。ConfigureAwait(false) 控制延续上下文,不负责取消、不保证另开线程,也不修复共享数据竞态。
多个独立操作可先启动再 await Task.WhenAll;它等待所有任务完成,并不会在某个任务失败时自动取消其他任务。若希望首错停止,需要共享 CancellationTokenSource、在工作中观察 token,并在返回前等待所有已启动任务收尾;仅 Cancel 后立刻返回会留下仍在运行的工作。本项目逐行处理,不为展示并发而并行修改同一字典。
CancellationToken 与协作停止
取消是一条请求,不是杀线程。拥有 CancellationTokenSource 的上层决定何时取消,下层接收 Token。可取消 I/O 要把 token 传给对应重载;循环在开始下一轮之前 ThrowIfCancellationRequested。若底层操作不接受 token,它可能一直运行,不能把包一层 WaitAsync 或等待超时当成停止操作本身。超时和取消也不等于回滚:已写入的文件字节不会自动撤回。
本项目 SummarizeAsync(TextReader,CancellationToken) 在读取前检查并将 token 传给 ReadLineAsync;测试既验证预先取消,也用可控制的 Reader 验证读取中取消。测试通过 TaskCompletionSource 等待真实读取开始后再 Cancel,最后等待工作结束;不依赖“睡10毫秒应该开始了”。看门狗5秒是测试死锁保护,不是业务超时。
CLI 通过 Console.CancelKeyPress 接收用户 Ctrl+C,将默认立即退出改为请求取消;finally 移除事件,防止重复调用时留下持有对象的处理器。异步取消通常抛 OperationCanceledException,捕获时应确认属于预期 token 的取消,复杂多任务场景不能吞掉不相关取消。这里进程只管理一项工作,统一返回2并写错误说明。
IDisposable、using 与借用
GC 只保证托管内存可被回收,不保证及时关闭文件句柄。using var reader = ... 在作用域退出时调用 Dispose,正常、异常、取消都走清理;实现 IAsyncDisposable 的资源可使用 await using 等待异步释放。Dispose 与 Cancel 是不同协议:取消要求工作停止,释放要求资源收回;不要在其他任务仍使用资源时先 Dispose。
函数若接收外部 Reader,应说明是否拥有它。本项目 Report 只借用 Reader,不关闭;CLI 创建 Reader 并在结果返回/异常后关闭。否则调用方继续使用同一 Reader 会遇到 ObjectDisposedException。代码中不接受一个含义不清的布尔“自动关闭”参数来隐藏所有权;建立清楚的调用边界更直接。
StreamReader 的 UTF8Encoding(false,true) 要求错误字节抛异常,禁用BOM探测使课程严格接受无BOM UTF-8;ASCII字段是业务格式,不是声称程序不支持任何Unicode。完整项目在 源码与测试,读者可同时核对库与入口的责任。
自检:给出可验证的退出条件
练习:在循环中删掉 token 检查但仍传给 ReadLineAsync,会不会一定失去取消?若自定义 Reader 忽略 token 呢?如果 Task.WhenAll 的一个任务失败,其他任务是否自动停?如果把 using 移进库,哪一个测试或调用会发现问题?
参考解答
是否取消取决于具体 Reader 是否观察 token;显式检查让已请求取消在下一轮读取前就被发现,不能替代不可取消底层操作。WhenAll不会自动取消兄弟任务;需请求取消后等待退出。现有测试读取 Reader 到末尾后再调用 ReadLineAsync,仍应返回 null;库提前 Dispose 将使调用失败。更全面的所有权测试还应覆盖错误路径和多次借用。清理应与创建资源的层保持一致。
依据:中文异步编程、协作取消、using、Task.WhenAll。核对2026-10-17。
继续阅读
- 开发实践
- 知识专题
- 知识专题