C# 试题 34:取消、异常与上下文
0661 以下代码输出什么?
难度: 实战
var cts = new CancellationTokenSource();
Task t = Task.Run(() =>
{
Thread.Sleep(300);
Console.WriteLine("finish");
});
cts.Cancel();
await t;
- A. 任务被中断,不输出
finish - B. 抛
OperationCanceledException - C. 输出
finish,取消是协作式的,Cancel 不会中止正在运行的代码 - D. 编译失败
查看答案与解析
正确答案C
解析: 取消是协作式的:Cancel() 只翻转 token 状态,不会中断或杀死正在运行的代码;任务代码没有检查 token 就继续执行完,输出 finish。想要真正停止,代码必须周期性地检查 IsCancellationRequested 或调用 ThrowIfCancellationRequested。
关键点: Cancel 是“请求”,不是“终止”;响应与否取决于任务代码。
0662 以下代码输出什么?
难度: 进阶
var cts = new CancellationTokenSource();
cts.Cancel();
Task t = Task.Run(() => cts.Token.ThrowIfCancellationRequested(), cts.Token);
try { await t; } catch (OperationCanceledException) { }
Console.WriteLine(t.Status);
- A.
Canceled,抛出的 OCE 携带的 token 与任务的 token 一致 - B.
Faulted - C.
RanToCompletion - D.
WaitingToRun
查看答案与解析
正确答案A
解析: 委托抛出的 OperationCanceledException 若携带的 token 与创建任务时传入的 token 是同一个,运行时就把任务标记为 Canceled 而不是 Faulted;await 它抛 TaskCanceledException。
关键点: 取消身份匹配是任务进入 Canceled 状态的前提。
0663 以下代码输出什么?
难度: 实战
var cts = new CancellationTokenSource();
cts.Cancel();
Task t = Task.Run(() => cts.Token.ThrowIfCancellationRequested());
Console.WriteLine(t.Status);
- A.
Canceled - B.
WaitingToRun - C.
Faulted,OCE 携带的 token 与任务关联的 token(此处为 None)不一致,任务按故障处理 - D.
RanToCompletion
查看答案与解析
正确答案C
解析: Task.Run 未传 token 时任务关联的 token 是 CancellationToken.None;委托抛出的 OCE 携带的是 cts.Token,两者不匹配,运行时无法确认这是“本次任务的取消”,于是把任务标记为 Faulted。想获得 Canceled 状态必须把同一个 token 传给 Task.Run。
关键点: 取消身份不一致 = 取消被降级成故障。
0664 以下代码输出什么?
难度: 进阶
var cts = new CancellationTokenSource();
cts.Cancel();
Console.WriteLine("a");
cts.Token.Register(() => Console.WriteLine("b"));
Console.WriteLine("c");
- A.
a/b/c,Register 遇到已取消的 token 会同步执行回调 - B.
a/c,已取消后注册不再回调 - C. 编译失败
- D. 抛
ObjectDisposedException
查看答案与解析
正确答案A
解析: Register 在 token 已处于取消状态时不会拒绝注册,而是在当前线程上同步调用回调:因此输出 a、b、c。这也是“注册回调的时机”容易踩的坑——回调可能在 Register 返回前就已经执行。
关键点: 已取消 token 的 Register 同步触发回调,注册前要处理好状态。
0665 以下代码输出什么?
难度: 实战
var cts = new CancellationTokenSource();
var reg = cts.Token.Register(() => Console.WriteLine("cb"));
reg.Dispose();
cts.Cancel();
Console.WriteLine("done");
- A.
cb/done - B.
cb后抛异常 - C.
done,Dispose 注销回调,Cancel 不再调用它 - D. 编译失败
查看答案与解析
正确答案C
解析: Register 返回的 CancellationTokenRegistration 实现 IDisposable:Dispose() 把回调从注册表中移除(若尚未触发),之后 Cancel 不会调用它。释放类时应把注册当作资源一并释放,避免回调泄漏到取消时刻。
关键点: 取消注册要 Dispose,否则回调可能在使用期外被触发。
0666 以下代码输出什么?
难度: 进阶
var cts = new CancellationTokenSource();
cts.Token.Register(() => throw new InvalidOperationException("boom"));
try { cts.Cancel(); }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
- A. 异常被吞掉
- B.
InvalidOperationException,回调异常原样传播 - C. 编译失败
- D.
AggregateException,Cancel 把回调抛出的异常包装成 AggregateException
查看答案与解析
正确答案D
解析: 取消回调在调用 Cancel 的线程上同步执行;回调抛异常时 Cancel 会把它包装成 AggregateException 抛给调用者(有多个回调异常时聚合全部)。因此注册回调里不应抛异常,否则 Cancel 的调用方会“无故”收到异常。
关键点: 回调异常以 AggregateException 形式从 Cancel 逃逸。
0667 以下代码输出什么?
难度: 实战
var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
try { await Task.Delay(10_000, cts.Token); }
catch (OperationCanceledException) { Console.WriteLine("cancelled"); }
- A. 等待 10 秒后输出
cancelled - B. 约 50ms 后输出
cancelled,CTS 定时自动取消 - C. 抛
TimeoutException - D. 编译失败
查看答案与解析
正确答案B
解析: new CancellationTokenSource(TimeSpan) 内部起一个定时器,到期自动调用 Cancel;Task.Delay(10_000, cts.Token) 收到取消后抛 OperationCanceledException,约 50ms 后输出 cancelled。
关键点: CTS 构造参数是“自动取消倒计时”,不是超时异常机制。
0668 以下代码输出什么?
难度: 进阶
var cts = new CancellationTokenSource();
cts.Cancel();
try { await Task.Delay(1000, cts.Token); }
catch (Exception e) { Console.WriteLine(e.GetType().Name); }
- A.
OperationCanceledException - B.
AggregateException - C.
TimeoutException - D.
TaskCanceledException
查看答案与解析
正确答案D
解析: Task.Delay 被取消时抛的是 TaskCanceledException(它继承自 OperationCanceledException),catch (Exception) 捕获到的具体类型是 TaskCanceledException。按 OperationCanceledException 捕获能覆盖两种取消,但诊断时要区分具体类型。
关键点: 捕获 OCE 能兜住取消,但实际抛出的常常是 TaskCanceledException。
0669 任务的异常从未被观察(未 await、未访问 Exception),会发生什么?
难度: 进阶
- A. 进程立即崩溃
- B. 默认不崩溃;任务被 GC 回收时触发 UnobservedTaskException 事件(.NET 4.5+ 默认不终止进程)
- C. 异常自动重抛到发起线程
- D. 异常静默消失且无任何通知
查看答案与解析
正确答案B
解析: 未被观察的任务异常不会同步冒泡;任务被垃圾回收时运行时引发 TaskScheduler.UnobservedTaskException 事件,.NET 4.5 起默认不因此终止进程。但它仍是 bug:日志里观察不到、异常被静默吞掉,应显式观察或 await。
关键点: 未观察异常会延迟到 GC 才暴露,别依赖“没人看就没事”。
0670 await 的延续默认在哪里执行?
难度: 实战
- A. 永远在发起线程
- B. 永远在线程池
- C. 捕获的 SynchronizationContext;无同步上下文则当前 TaskScheduler;都没有则线程池
- D. 在新建的专用线程
查看答案与解析
正确答案C
解析: await 在等待点捕获“上下文”:先看 SynchronizationContext.Current(UI/旧 ASP.NET 有),没有则看当前 TaskScheduler,都没有(如控制台)就在线程池上调度延续。ConfigureAwait(false) 的作用就是跳过前两步。
关键点: 延续的线程由“捕获的上下文”决定,不是固定的。
0671 关于 OperationCanceledException 与 TaskCanceledException 的关系,哪项正确?
难度: 进阶
- A. 两者毫无关系
- B. OperationCanceledException 继承自 TaskCanceledException
- C. 两者是同一类型
- D. TaskCanceledException 继承自 OperationCanceledException,捕获 OCE 即可覆盖两类取消
查看答案与解析
正确答案D
解析: TaskCanceledException : OperationCanceledException。取消任务时(Task.Delay、WhenAll 遇取消等)抛 TaskCanceledException;catch (OperationCanceledException) 能同时捕获两者,但要注意它也会捕获用户代码自己抛的 OCE,可能掩盖“不是取消”的路径。
关键点: 捕获 OCE 覆盖两类取消,但要区分“取消源”再决定是否重抛。
0672 以下代码输出什么?
难度: 实战
var cts = new CancellationTokenSource();
Task t1 = Task.Delay(Timeout.Infinite, cts.Token);
Task t2 = Task.FromResult(1);
var all = Task.WhenAll(t1, t2);
cts.Cancel();
try { await all; } catch (OperationCanceledException) { }
Console.WriteLine(all.IsCanceled);
- A.
True,其中一个任务被取消时 WhenAll 结果也是 Canceled - B.
False - C. 抛
AggregateException - D. 编译失败
查看答案与解析
正确答案A
解析: WhenAll 等待全部任务:t2 已成功,t1 因取消而进入 Canceled;整体在全部完成时没有故障、但有任务被取消,于是 WhenAll 结果也是 Canceled,await 抛 TaskCanceledException。
关键点: 部分任务取消会“传染”给 WhenAll 的最终状态。
0673 以下代码输出什么?
难度: 进阶
var cts = new CancellationTokenSource();
var token = cts.Token;
var t = Task.Run(async () =>
{
await Task.Delay(10);
token.ThrowIfCancellationRequested();
return "ok";
}, token);
cts.Dispose(); // 只释放,未取消
Console.WriteLine(await t);
- A. 抛
ObjectDisposedException - B.
ok,Dispose 不等于 Cancel,已开始的操作用不到 token 时不受影响 - C. 抛
OperationCanceledException - D. 编译失败
查看答案与解析
正确答案B
解析: CancellationTokenSource.Dispose() 只释放内部资源(定时器、注册表),并不会取消操作;token 仍然是“未取消”状态,任务正常完成输出 ok。取消必须显式调用 Cancel() 或依赖 CancelAfter。
关键点: Dispose 与 Cancel 是两个独立动作,别混为一谈。
0674 以下代码输出什么?
难度: 进阶
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
cts2.Cancel();
Console.WriteLine(linked.IsCancellationRequested);
- A.
False - B. 抛
ObjectDisposedException - C.
True,任一源取消,链接的 CTS 也随之取消 - D. 编译失败
查看答案与解析
正确答案C
解析: CreateLinkedTokenSource 把多个 token 合并成一个:只要任一源 token 被取消,链接源也会进入取消状态。注意链接源本身要 Dispose 以解除对源的监听,避免回调泄漏。
关键点: 多条件取消用链接源,“任一成立即取消”。
0675 以下代码输出什么?
难度: 实战
Task t = Task.Delay(10_000);
try { await t.WaitAsync(TimeSpan.FromMilliseconds(50)); }
catch (TimeoutException) { Console.WriteLine("timeout"); }
- A. 输出
timeout,WaitAsync 超时抛 TimeoutException,原任务继续运行 - B. 50ms 后输出
timeout,原任务被取消 - C. 抛
TaskCanceledException - D. 编译失败
查看答案与解析
正确答案A
解析: Task.WaitAsync(timeout)(.NET 6+)给等待加超时:超时抛 TimeoutException,但被等待的原任务仍在后台运行(不会被取消)。想要超时同时取消原任务,应传入自己的 CancellationToken 并在超时后自行 Cancel。
关键点: WaitAsync 超时 ≠ 取消原任务,两者要分开管理。
0676 以下代码输出什么?
难度: 实战
var cts = new CancellationTokenSource();
var token = cts.Token;
try
{
token.ThrowIfCancellationRequested();
Console.WriteLine("no");
}
catch (OperationCanceledException)
{
Console.WriteLine("cancelled");
}
- A. 输出
cancelled - B. 抛
TaskCanceledException - C. 编译失败
- D. 输出
no,未取消时 ThrowIfCancellationRequested 直接返回
查看答案与解析
正确答案D
解析: ThrowIfCancellationRequested 只在 IsCancellationRequested 为 true 时抛 OperationCanceledException;未取消时是空操作,继续执行输出 no。它是协作式取消的“检查点”。
关键点: 该方法平时零成本,取消时才抛异常。
0677 以下代码输出什么?
难度: 实战
var local = new AsyncLocal<int>();
local.Value = 1;
int v = await Task.Run(() => local.Value);
Console.WriteLine(v);
- A.
0,AsyncLocal 不流向子任务 - B. 编译失败
- C. 抛
InvalidOperationException - D.
1,AsyncLocal 随执行上下文流入 Task.Run 的任务
查看答案与解析
正确答案D
解析: AsyncLocal<T> 的值随 ExecutionContext 流动:Task.Run 创建的任务继承调用方的 AsyncLocal 快照,因此任务内读到 1。注意流是“复制下行”,任务内修改不会回流到调用方。
关键点: AsyncLocal 下传不回流,跨异步边界传递“请求上下文”时要注意。
0678 以下代码输出什么?
难度: 进阶
Task t1 = Task.FromException(new InvalidOperationException("A"));
Task t2 = Task.FromException(new ArgumentException("B"));
var all = Task.WhenAll(t1, t2);
try { await all; } catch (Exception) { }
Console.WriteLine(all.Exception!.InnerExceptions.Count);
- A.
1,只记录第一个异常 - B.
0 - C.
2,WhenAll 聚合所有任务的异常;await 只解包抛出第一个 - D. 抛
NullReferenceException
查看答案与解析
正确答案C
解析: WhenAll 把每个任务的异常都收进结果的 AggregateException(这里 2 个);await 只把第一个内部异常解包抛出,其余异常只有通过 all.Exception 才能观察。只 catch await 会漏掉其他任务的异常。
关键点: await 只能拿到“第一个”,完整异常列表在 Exception 属性里。
0679 以下代码输出什么?
难度: 实战
static async Task M() => throw new OperationCanceledException();
Task t = M();
Console.WriteLine(t.Status);
- A.
Canceled,async 方法抛 OCE 时返回的任务进入 Canceled 状态 - B.
Faulted - C.
RanToCompletion - D. 异常同步抛到调用方
查看答案与解析
正确答案A
解析: async 状态机识别到 OperationCanceledException 时,把返回的任务置为 Canceled(而非 Faulted),await 它抛 TaskCanceledException。这与“取消身份”规则一致:OCE 语义上等于“任务被取消”。
关键点: async 方法抛 OCE = Canceled 状态,区别于普通异常。
0680 以下代码输出什么?
难度: 进阶
var cts = new CancellationTokenSource();
cts.Cancel();
try { await Task.Delay(1000, cts.Token); }
catch (OperationCanceledException e)
{
Console.WriteLine(e is TaskCanceledException);
}
- A.
False - B. 编译失败
- C. 捕获不到异常
- D.
True,Task.Delay 取消时抛 TaskCanceledException,它是 OCE 的子类
查看答案与解析
正确答案D
解析: 用 catch (OperationCanceledException) 能捕获 Task.Delay 的取消异常,且该异常的实际类型是 TaskCanceledException(OCE 的子类),is 判断为 True。这也说明:想区分“谁的取消”,应检查异常携带的 CancellationToken。
关键点: 按 OCE 捕获没问题,但具体类型与取消来源要会用 token 分辨。