Task.Run + Task.WhenAll 和 Parallel 之二
在混合使用 Task.Run
+ Task.WhenAll
和 Parallel
时,处理取消需要协调多个取消令牌源(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) |
分块处理 | 每个分块开始前检查取消 | 分块大小影响取消响应速度 |
资源清理 | 在catch 或finally 块中释放文件句柄/内存映射等资源 |
使用using 语句辅助管理 |
异常传递 | 捕获OperationCanceledException 后重新抛出 |
避免吞没取消异常 |
六、混合取消的最佳实践
-
取消令牌传递规则:
// 所有方法签名显式包含CancellationToken void ProcessChunk(..., CancellationToken token)
-
高频检查点优化:
// 每处理N个元素检查一次 for (int i = 0; i < data.Length; i++) { if (i % 1000 == 0) token.ThrowIfCancellationRequested(); // ... }
-
取消响应分级:
// 紧急取消(立即终止) token.ThrowIfCancellationRequested(); // 温和取消(完成当前分块后终止) if (token.IsCancellationRequested) { Cleanup(); break; }
-
跨线程取消监控:
// 注册取消回调(如释放资源) 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.WhenAll
和 Parallel
时处理取消,需要采用分层取消策略和令牌链接技术。以下是完整的解决方案:
一、分层取消架构设计
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();
}
}
七、关键注意事项
-
令牌传递一致性:
- 所有方法签名都应包含
CancellationToken token
参数 - 异步方法使用
Async
后缀并支持取消
- 所有方法签名都应包含
-
资源泄漏防护:
// 使用using确保资源释放 using var fileStream = new FileStream(..., token); using var ctsLink = CancellationTokenSource.CreateLinkedTokenSource(...);
-
取消响应速度:
- CPU密集型循环:每100-1000次迭代检查一次
- I/O密集型操作:在所有
await
前传递令牌 - 分块处理:在分块边界处检查
-
混合异常处理:
- 使用
AggregateException.Flatten()
处理嵌套异常 - 通过
ExceptionDispatchInfo
保留原始堆栈
- 使用
这种设计确保:
- 用户点击一次取消按钮即可停止所有并行任务
- 大文件的分块处理能及时中断
- 小文件的批量处理能立即终止
- 所有资源都能正确释放
- 取消状态能跨线程同步
通过分层令牌链接和统一的取消检查策略,可以实现混合并行任务的安全高效取消。