C#中CancellationToken为何表现出非确定性行为?
关于Task取消行为差异的原因分析
现象总结
- 将
tokenSource.Cancel()移至Task.Run()调用前执行时,程序必然触发OperationCanceledException并进入catch块 - 保持原代码顺序(先创建Task再调用Cancel)时,程序有时进入catch块,有时会无限循环打印数字
原因拆解
1. Cancel在Task.Run之前执行的场景
当你先调用tokenSource.Cancel(),此时传入Task.Run的CancellationToken已经处于已取消状态。Task.Run在调度任务前会先检查令牌状态:如果令牌已取消,它不会执行传入的委托逻辑,直接返回一个处于已取消状态的Task。当你await这个Task时,框架会自动抛出OperationCanceledException,因此每次都会进入catch块。
2. 原代码顺序的场景
原代码存在线程调度竞态条件,两种结果取决于Cancel调用和任务委托执行的先后:
- 若
tokenSource.Cancel()在任务委托的while循环开始执行之前被调用:Task.Run检测到令牌已取消,不会执行委托,返回已取消的Task,await时抛出异常进入catch块。 - 若线程池线程已经调度并开始执行
while(true)循环:你的委托逻辑里完全没有检查CancellationToken的状态,任务会一直执行无限循环,不会响应任何取消请求,因此会持续打印数字,永远不会进入catch块。
修正方案
要让任务正确响应取消,必须在循环内部主动检查令牌状态,修改后的代码如下:
using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main() { var tokenSource = new CancellationTokenSource(); var task = Task.Run(() => { int i = 0; while (true) { // 主动检查令牌,若已取消则抛出异常终止任务 tokenSource.Token.ThrowIfCancellationRequested(); Console.WriteLine($"{i++}"); } }, tokenSource.Token); tokenSource.Cancel(); try { await task; } catch (OperationCanceledException e) { Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {e.Message}"); } finally { tokenSource.Dispose(); } Console.ReadKey(); } }
这样无论Cancel调用时机如何,只要令牌被取消,循环内的检查会触发异常,任务会进入取消状态,await时就能捕获异常。
内容的提问来源于stack exchange,提问作者user33276346
相关产品推荐
相关产品推荐

