You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:36:35