直接说结论:绝大多数情况下该用
await,
ContinueWith只在极少数需要精细控制延续调度、或必须绕过 async/await 语法限制的场景才用——比如写底层异步基础设施、适配旧回调 API、或手动构建状态机。
什么时候会不小心掉进 ContinueWith
的坑
常见错误现象:UI 线程更新崩溃、异常“消失”、任务链意外并行执行、上下文(如
HttpContext或 UI 同步上下文)丢失。
ContinueWith默认不捕获当前
SynchronizationContext,UI 控件赋值(如
label.Text = "done")会直接抛
InvalidOperationException它不会“解包”
AggregateException,你得手动检查
t.Exception?.InnerExceptions,否则异常被吞掉 多个
ContinueWith链式调用时,每个延续都新建一个
Task,容易造成任务嵌套过深、调试困难 没有
await的隐式
ConfigureAwait(false)优化,线程调度更重、开销略高
await
真正省掉的不是代码行数,而是心智负担
它自动处理三件事:上下文恢复、异常扁平化、状态机挂起/恢复。而
ContinueWith全得你手写。 UI 方法里写
await DoWork()→ 后续代码自动回到 UI 线程执行;用
ContinueWith必须显式传
TaskScheduler.FromCurrentSynchronizationContext()
await Task.WhenAll(t1, t2, t3)抛出单个
Exception;
Task.WhenAll(...).ContinueWith(...)中若任一任务失败,
t.Result会直接 throw,但你得自己 try/catch 包裹整个延续逻辑 循环中 await 是自然的:
while (cond) { await LoadNext(); };用 ContinueWith模拟等价逻辑要递归构造任务链,极易栈溢出或逻辑错乱
真要用 ContinueWith
的两个典型场景
不是“想用”,而是“不得不”。其他情况基本都是过早优化或历史包袱。
对接无 async 支持的旧库:比如某 SDK 只提供Task FooAsync()+
Action<task> onCompleted</task>回调,这时用
.ContinueWith(t => { ... }) 是最直白的胶水层
需要非默认调度策略:例如你想确保延续一定在后台线程跑(避开 UI 上下文),且不希望受 ConfigureAwait(false)影响,可明确传
TaskScheduler.Default——
await本身不提供这种粒度
task.ContinueWith(t =>
{
// 这里一定在 ThreadPool 线程执行,与调用线程无关
ProcessInBackground(t.Result);
}, TaskScheduler.Default);
真正复杂的点从来不在语法选择上,而在你是否清楚自己正在放弃什么:用
ContinueWith就等于主动接管调度、异常传播和上下文生命周期——这些事
await默默替你扛了五年以上。除非你盯着 IL 看过编译器生成的状态机,否则别轻易动它。
