使用WhenAll时取消任务引发TaskWasCancelledException的解决方案咨询
解决TaskWasCancelledException及任务null问题的方案
咱们先拆解你当前代码里的几个核心问题,再给出针对性的优化方案:
1. 为什么会抛出TaskWasCancelledException?
Task.WhenAll会等待所有任务完成,而被你主动取消的任务会处于Canceled状态——.NET会把这类取消异常包装进AggregateException抛出,这是框架的正常行为,因为它认为“任务未按预期正常完成”,但对你来说这是预期内的操作。
2. 为什么会出现task != null的情况?
你用Parallel.ForEach并行往List<Task>里添加任务,但List<T>不是线程安全的!并行添加操作可能导致集合内部状态混乱,甚至出现元素为null的异常(虽然Task.Run永远不会返回null,但线程不安全的集合操作会搞砸元素的存储逻辑)。
优化后的实现方案
我们可以重构代码,把每个名称的匹配逻辑封装成独立的异步方法,同时解决线程安全、取消处理和重复添加的问题:
步骤1:封装单名称的匹配逻辑
针对每个名称,用一个共享的CancellationTokenSource,任一匹配成功就取消所有任务,同时确保只添加一次结果:
private async Task ProcessAccount(Account account, Regex reg1, Regex reg2, Regex reg3, ConcurrentBag<Account> bag) { using var cts = new CancellationTokenSource(); var token = cts.Token; // 定义三个匹配任务,先检查取消状态再执行匹配 var task1 = Task.Run(() => { if (token.IsCancellationRequested) return false; var isMatch = reg1.IsMatch(account.DisplayName); if (isMatch) cts.Cancel(); // 匹配成功就取消其他任务 return isMatch; }, token); var task2 = Task.Run(() => { if (token.IsCancellationRequested) return false; var isMatch = reg2.IsMatch(account.DisplayName); if (isMatch) cts.Cancel(); return isMatch; }, token); var task3 = Task.Run(() => { if (token.IsCancellationRequested) return false; var isMatch = reg3.IsMatch(account.DisplayName); if (isMatch) cts.Cancel(); return isMatch; }, token); // 等待任一任务完成或取消 var completedTask = await Task.WhenAny(task1, task2, task3); // 检查是否有匹配成功的任务,再添加到集合 if (await completedTask) { bag.Add(account); } // 等待剩余任务完成,主动忽略取消异常 var remainingTasks = new[] { task1, task2, task3 }.Where(t => !t.IsCompleted); foreach (var task in remainingTasks) { try { await task; } catch (OperationCanceledException) { // 预期内的取消,无需处理 } } }
步骤2:并行处理所有账户
用Task.WhenAll代替Parallel.ForEach处理所有账户,更符合异步编程模型,同时避免线程安全问题:
// 为每个账户创建处理任务,然后等待全部完成 var tasks = accounts.Select(account => ProcessAccount(account, reg1, reg2, reg3, bag)).ToList(); await Task.WhenAll(tasks);
如果你想保留原有的Parallel.ForEach方式
至少要解决两个核心问题:
- 用线程安全集合存储任务
- 主动过滤取消异常:
// 用ConcurrentBag代替List,保证线程安全 var tasks = new ConcurrentBag<Task>(); Parallel.ForEach(accounts, p => { var can1 = new CancellationTokenSource(); var can2 = new CancellationTokenSource(); var can3 = new CancellationTokenSource(); tasks.Add(Task.Run(() => { if (reg1.IsMatch(p.DisplayName)) { bag.Add(p); can2.Cancel(); can3.Cancel(); } }, can1.Token)); tasks.Add(Task.Run(() => { if (reg2.IsMatch(p.DisplayName)) { bag.Add(p); can1.Cancel(); can3.Cancel(); } }, can2.Token)); tasks.Add(Task.Run(() => { if (reg3.IsMatch(p.DisplayName)) { bag.Add(p); can1.Cancel(); can2.Cancel(); } }, can3.Token)); }); // 逐个等待任务,忽略预期的取消异常 foreach (var task in tasks) { try { await task; } catch (OperationCanceledException) { // 主动取消的任务,无需处理 } catch (Exception ex) { // 处理其他意外异常 Console.WriteLine($"Unexpected error: {ex.Message}"); } }
关键优化点总结
- 线程安全集合:用
ConcurrentBag<Task>代替List<Task>存储任务,避免并行添加导致的null或集合损坏 - 预期取消处理:捕获
OperationCanceledException并忽略,因为这些是我们主动触发的取消操作 - 避免重复添加:用共享的
CancellationTokenSource确保任一匹配成功后立即取消其他任务,减少重复添加的概率 - 异步优先:尽量用
Task.WhenAll+Select的异步方式,比Parallel.ForEach更适合CPU密集型任务的调度
内容的提问来源于stack exchange,提问作者Muds
相关产品推荐
相关产品推荐

