Task.Run + Task.WhenAll 和 Parallel 之二

在混合使用 Task.Run + Task.WhenAllParallel 时,处理取消需要协调多个取消令牌源(CancellationTokenSource)并确保所有并行操作都能响应取消请求。以下是具体实现方案:

一、统一取消架构设计

public class MixedParallelProcessor
{
    // 全局取消令牌源
    private readonly CancellationTokenSource _globalCts = new();
    
    // 处理混合文件
    public async Task ProcessMixedFilesAsync(IEnumerable<string> files)
    {
        // 1. 分类文件
        var (largeFiles, smallFiles) = ClassifyFiles(files);
        
        // 2. 创建链接令牌(可绑定外部取消)
        var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            _globalCts.Token,
            new CancellationTokenSource(TimeSpan.FromMinutes(30)).Token // 超时取消
        );
        
        try
        {
            // 3. 并行执行大文件和小文件处理
            await Task.WhenAll(
                ProcessLargeFilesAsync(largeFiles, linkedCts.Token),
                ProcessSmallFilesAsync(smallFiles, linkedCts.Token)
            );
        }
        catch (OperationCanceledException)
        {
            // 处理取消逻辑
            Console.WriteLine("处理已被取消");
        }
    }
    
    // 外部取消方法
    public void Cancel() => _globalCts.Cancel();
}

二、大文件处理中的取消(Parallel.ForEach)

private Task ProcessLargeFilesAsync(
    List<string> largeFiles, 
    CancellationToken token)
{
    return Task.Run(() => 
    {
        try
        {
            var options = new ParallelOptions
            {
                CancellationToken = token, // 传递令牌
                MaxDegreeOfParallelism = 4
            };
            
            Parallel.ForEach(largeFiles, options, (file, state) =>
            {
                // 关键:定期检查取消
                token.ThrowIfCancellationRequested();
                
                // 处理单个大文件(内部也需传递token)
                ProcessSingleLargeFile(file, token);
            });
        }
        catch (OperationCanceledException) 
        {
            // 清理大文件处理中的资源
            CleanupLargeFiles();
            throw; // 重新抛出以被上层捕获
        }
    }, token); // 将令牌传递给Task.Run
}
private void ProcessSingleLargeFile(string file, CancellationToken token)
{
    using var mmap = MemoryMappedFile.CreateFromFile(file);
    using var accessor = mmap.CreateViewAccessor();
    
    long position = 0;
    const long chunkSize = 100 * 1024 * 1024; // 100MB分块
    
    while (position < accessor.Capacity)
    {
        // 每次分块前检查取消
        token.ThrowIfCancellationRequested();
        
        // 处理分块...
        ProcessChunk(accessor, position, Math.Min(chunkSize, accessor.Capacity - position), token);
        
        position += chunkSize;
    }
}

三、小文件处理中的取消(Task.WhenAll)

private async Task ProcessSmallFilesAsync(
    List<string> smallFiles,
    CancellationToken token)
{
    var tasks = new List<Task>();
    var throttler = new SemaphoreSlim(Environment.ProcessorCount * 2);
    
    foreach (var file in smallFiles)
    {
        // 每次迭代前检查取消
        token.ThrowIfCancellationRequested();
        
        // 获取信号量(带取消)
        await throttler.WaitAsync(token);
        
        tasks.Add(Task.Run(async () =>
        {
            try
            {
                // 处理单个小文件(传递令牌)
                await ProcessSingleSmallFileAsync(file, token);
            }
            finally
            {
                throttler.Release();
            }
        }, token));
    }
    
    // 等待所有任务完成(带取消监控)
    try
    {
        await Task.WhenAll(tasks);
    }
    catch
    {
        // 发生异常时取消剩余任务
        _globalCts.Cancel();
        throw;
    }
}
private async Task ProcessSingleSmallFileAsync(string file, CancellationToken token)
{
    // 读取文件(支持取消)
    var data = await File.ReadAllBytesAsync(file, token);
    
    // CPU处理部分(同步操作中定期检查取消)
    await Task.Run(() => 
    {
        for (int i = 0; i < data.Length; i++)
        {
            // 每1000次迭代检查一次取消
            if (i % 1000 == 0) token.ThrowIfCancellationRequested();
            
            // 处理数据...
        }
    }, token);
}

四、混合取消的异常处理策略

public async Task SafeProcessing(IEnumerable<string> files)
{
    try
    {
        await ProcessMixedFilesAsync(files);
    }
    catch (OperationCanceledException ex)
    {
        // 区分取消类型
        if (_globalCts.IsCancellationRequested)
            Log("用户手动取消");
        else if (ex.CancellationToken.IsCancellationRequested)
            Log($"超时取消或内部取消");
    }
    catch (AggregateException ae)
    {
        // 解包Parallel抛出的AggregateException
        ae.Flatten().Handle(ex => 
        {
            if (ex is OperationCanceledException oce)
            {
                Log("并行循环取消");
                return true;
            }
            return false;
        });
    }
    finally
    {
        // 释放全局CTS
        _globalCts.Dispose(); 
    }
}

五、关键取消技术点总结

场景 取消实现方式 注意事项
全局取消 CancellationTokenSource控制所有操作 使用CreateLinkedTokenSource合并令牌
大文件(Parallel) ParallelOptions.CancellationToken + 循环内定期调用token.ThrowIfCancellationRequested() 避免在长时间操作中不检查取消
小文件(Task) CancellationToken传递给Task.Run和异步I/O方法 配合SemaphoreSlim.WaitAsync(token)
分块处理 每个分块开始前检查取消 分块大小影响取消响应速度
资源清理 catchfinally块中释放文件句柄/内存映射等资源 使用using语句辅助管理
异常传递 捕获OperationCanceledException后重新抛出 避免吞没取消异常

六、混合取消的最佳实践

  1. 取消令牌传递规则

    // 所有方法签名显式包含CancellationToken
    void ProcessChunk(..., CancellationToken token)
    
  2. 高频检查点优化

    // 每处理N个元素检查一次
    for (int i = 0; i < data.Length; i++)
    {
        if (i % 1000 == 0) token.ThrowIfCancellationRequested();
        // ...
    }
    
  3. 取消响应分级

    // 紧急取消(立即终止)
    token.ThrowIfCancellationRequested();
    
    // 温和取消(完成当前分块后终止)
    if (token.IsCancellationRequested) 
    {
        Cleanup();
        break;
    }
    
  4. 跨线程取消监控

    // 注册取消回调(如释放资源)
    token.Register(() => 
    {
        File.Delete(tempFile); // 取消时删除临时文件
    });
    

七、完整生命周期示例

public async Task RunMixedProcessingWithCancellation()
{
    var processor = new MixedParallelProcessor();
    var files = GetFiles();
    
    // 设置超时(30秒后自动取消)
    var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
    
    // 用户取消(如按钮点击)
    var userCts = new CancellationTokenSource();
    
    // 组合令牌
    var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        timeoutCts.Token,
        userCts.Token
    );
    
    try
    {
        await processor.ProcessMixedFilesAsync(files, linkedCts.Token);
    }
    finally
    {
        linkedCts.Dispose();
        timeoutCts.Dispose();
        userCts.Dispose();
    }
}
// 用户触发的取消
private void btnCancel_Click(object sender, EventArgs e)
{
    userCts.Cancel(); // 触发取消
}

通过这种设计,混合并行任务可以实现:

  • 统一取消入口:一个操作取消所有并行任务
  • 资源安全:及时释放文件句柄和内存
  • 响应迅速:在分块边界和迭代检查点立即响应
  • 异常隔离:单个文件处理失败不影响整体

在混合使用 Task.Run + Task.WhenAllParallel 时处理取消,需要采用分层取消策略和令牌链接技术。以下是完整的解决方案:


一、分层取消架构设计

public class ParallelProcessor
{
    // 全局取消令牌源(用户级取消)
    private readonly CancellationTokenSource _globalCts = new();
    
    // 处理混合任务
    public async Task ProcessAsync(IEnumerable<string> files)
    {
        // 创建分层令牌(链接全局令牌)
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
            _globalCts.Token,
            new CancellationTokenSource(TimeSpan.FromMinutes(30)).Token // 超时取消
        );
        
        try
        {
            // 分类处理
            var (largeFiles, smallFiles) = ClassifyFiles(files);
            
            // 并行执行两类处理
            await Task.WhenAll(
                ProcessLargeFilesAsync(largeFiles, linkedCts.Token),
                ProcessSmallFilesAsync(smallFiles, linkedCts.Token)
            );
        }
        catch (OperationCanceledException ex) when (ex.CancellationToken == linkedCts.Token)
        {
            // 统一处理取消逻辑
            CleanupResources();
            Log("Operation canceled");
        }
    }
    
    // 外部取消入口
    public void Cancel() => _globalCts.Cancel();
}

二、大文件处理中的取消(Parallel)

private Task ProcessLargeFilesAsync(
    List<string> largeFiles, 
    CancellationToken token)
{
    return Task.Run(() => 
    {
        var options = new ParallelOptions {
            CancellationToken = token, // 传递取消令牌
            MaxDegreeOfParallelism = 4
        };
        
        try
        {
            Parallel.ForEach(largeFiles, options, (file, state) => 
            {
                // 关键点1:定期检查取消
                token.ThrowIfCancellationRequested();
                
                // 处理文件(内部方法也需传递token)
                ProcessSingleLargeFile(file, token);
            });
        }
        catch (OperationCanceledException) 
        {
            // 大文件特有清理逻辑
            ReleaseMemoryMappings();
            throw; // 重新抛出
        }
    }, token); // 将令牌传递给Task.Run
}

private void ProcessSingleLargeFile(string file, CancellationToken token)
{
    using var mmap = MemoryMappedFile.CreateFromFile(file);
    using var accessor = mmap.CreateViewAccessor();
    
    long position = 0;
    const long chunkSize = 100 * 1024 * 1024; // 100MB分块
    
    while (position < accessor.Capacity)
    {
        // 关键点2:每个分块前检查取消
        token.ThrowIfCancellationRequested();
        
        // 处理分块...
        ProcessChunk(accessor, position, Math.Min(chunkSize, accessor.Capacity - position), token);
        
        position += chunkSize;
    }
}

三、小文件处理中的取消(Task.WhenAll)

private async Task ProcessSmallFilesAsync(
    List<string> smallFiles,
    CancellationToken token)
{
    var tasks = new List<Task>();
    var throttler = new SemaphoreSlim(Environment.ProcessorCount * 2);
    
    foreach (var file in smallFiles)
    {
        // 关键点1:迭代前检查取消
        token.ThrowIfCancellationRequested();
        
        await throttler.WaitAsync(token); // 带取消的等待
        
        tasks.Add(ProcessSingleSmallFileAsync(file, throttler, token));
    }
    
    try
    {
        await Task.WhenAll(tasks);
    }
    catch
    {
        // 发生异常时取消所有任务
        _globalCts.Cancel();
        throw;
    }
}

private async Task ProcessSingleSmallFileAsync(
    string file, 
    SemaphoreSlim throttler,
    CancellationToken token)
{
    try
    {
        // 关键点2:传递令牌到所有异步操作
        var data = await File.ReadAllBytesAsync(file, token);
        
        // CPU处理部分
        await Task.Run(() => 
        {
            for (int i = 0; i < data.Length; i++)
            {
                // 关键点3:密集循环中定期检查
                if (i % 1000 == 0) token.ThrowIfCancellationRequested();
                
                // 处理数据...
            }
        }, token);
    }
    finally
    {
        throttler.Release();
    }
}

四、混合取消的核心技术点

1. 令牌传递金字塔

graph TD A[全局CancellationTokenSource] --> B[链接令牌源] B --> C[大文件处理Task.Run] B --> D[小文件处理Task.WhenAll] C --> E[Parallel.ForEach] E --> F[单个大文件处理方法] F --> G[分块处理循环] D --> H[单个小文件Task] H --> I[文件读取Async] H --> J[CPU处理Task.Run]

2. 取消检查策略

场景 检查方式 频率
循环/迭代开始 token.ThrowIfCancellationRequested() 每次迭代
长时间同步操作 if (i % N == 0) token.ThrowIfCancellationRequested() 每N次迭代
异步等待前 await throttler.WaitAsync(token) 每次等待前
并行任务启动 Task.Run(..., token) 任务创建时
I/O操作 File.ReadAllBytesAsync(file, token) 异步调用时
分块边界 分块处理前检查 每个分块开始前

3. 资源清理技术

// 注册取消回调
token.Register(() => 
{
    // 1. 关闭内存映射
    foreach (var accessor in _activeAccessors)
        accessor.Dispose();
    
    // 2. 删除临时文件
    File.Delete(_tempFilePath);
    
    // 3. 重置状态
    _processingState = State.Canceled;
});

五、混合取消最佳实践

1. 统一异常处理

try
{
    await processor.ProcessAsync(files);
}
catch (OperationCanceledException ex) when (ex.CancellationToken == processor.CancellationToken)
{
    // 用户主动取消
    ShowToast("操作已取消");
}
catch (AggregateException ae) when (ae.InnerExceptions.Any(e => e is OperationCanceledException))
{
    // 处理Parallel抛出的聚合异常
    var cancelEx = ae.Flatten().InnerExceptions.OfType<OperationCanceledException>().First();
    Log($"并行操作取消: {cancelEx.Message}");
}
catch (Exception ex)
{
    // 其他异常处理
}

2. 取消响应分级

// 立即取消 (关键点)
token.ThrowIfCancellationRequested();

// 优雅取消 (完成当前单元)
if (token.IsCancellationRequested)
{
    SavePartialResults();
    break;
}

3. 跨线程状态同步

// 使用原子操作标记状态
private volatile bool _isCanceling;

void Cancel()
{
    _globalCts.Cancel();
    Interlocked.Exchange(ref _isCanceling, 1);
}

// 在CPU循环中检查
for (int i = 0; i < data.Length; i++)
{
    if (Volatile.Read(ref _isCanceling)) 
        return partialResults;
}

六、完整生命周期示例

public async Task RunMixedProcessing()
{
    var cts = new CancellationTokenSource();
    var processor = new ParallelProcessor();

    // UI取消按钮事件
    btnCancel.Click += (s, e) => processor.Cancel();
    
    try
    {
        // 启动处理
        await processor.ProcessAsync(GetFiles(), cts.Token);
    }
    catch (OperationCanceledException)
    {
        // 显示取消状态
        UpdateUI("已取消");
    }
    finally
    {
        // 释放资源
        processor.Dispose();
        cts.Dispose();
    }
}

七、关键注意事项

  1. 令牌传递一致性

    • 所有方法签名都应包含 CancellationToken token 参数
    • 异步方法使用 Async 后缀并支持取消
  2. 资源泄漏防护

    // 使用using确保资源释放
    using var fileStream = new FileStream(..., token);
    using var ctsLink = CancellationTokenSource.CreateLinkedTokenSource(...);
    
  3. 取消响应速度

    • CPU密集型循环:每100-1000次迭代检查一次
    • I/O密集型操作:在所有 await 前传递令牌
    • 分块处理:在分块边界处检查
  4. 混合异常处理

    • 使用 AggregateException.Flatten() 处理嵌套异常
    • 通过 ExceptionDispatchInfo 保留原始堆栈

这种设计确保:

  • 用户点击一次取消按钮即可停止所有并行任务
  • 大文件的分块处理能及时中断
  • 小文件的批量处理能立即终止
  • 所有资源都能正确释放
  • 取消状态能跨线程同步

通过分层令牌链接和统一的取消检查策略,可以实现混合并行任务的安全高效取消。

posted @ 2025-08-17 19:42  青云Zeo  阅读(15)  评论(0)    收藏  举报