1. .NET TPL 中的 Task.Factory 延续任务深度解析
在 WinForms 应用开发中,我们经常需要处理复杂的异步任务流。想象这样一个场景:你的应用需要同时从多个股票 API 获取实时数据,然后合并结果更新 UI,同时还要确保不阻塞主线程。这时候,Task.Factory 的 ContinueWhenAll 和 ContinueWhenAny 方法就能大显身手了。
1.1 延续任务的核心机制
延续任务(Continuation Tasks)是 TPL 中一个强大的功能,它允许我们在一个或多个任务完成后自动触发后续操作。这就像是在说:"等这些任务都完成了,就执行这个操作"。
ContinueWhenAll 和 ContinueWhenAny 的主要区别在于触发条件:
- ContinueWhenAll:等待所有前置任务完成
- ContinueWhenAny:只要有一个前置任务完成就触发
csharp复制// 基本用法示例
var tasks = new Task[3];
tasks[0] = Task.Run(() => GetStockPrice("MSFT"));
tasks[1] = Task.Run(() => GetStockPrice("AAPL"));
tasks[2] = Task.Run(() => GetStockPrice("GOOG"));
// 所有任务完成后执行
Task.Factory.ContinueWhenAll(tasks, completedTasks => {
// 合并结果
var prices = completedTasks.Select(t => t.Result);
UpdateStockChart(prices);
});
1.2 关键配置选项
TaskContinuationOptions 提供了精细的控制:
csharp复制Task.Factory.ContinueWhenAll(tasks, completedTasks => {
// 只在所有任务成功完成时执行
}, TaskContinuationOptions.OnlyOnRanToCompletion);
常用选项包括:
- OnlyOnRanToCompletion:仅成功时执行
- OnlyOnFaulted:仅失败时执行
- ExecuteSynchronously:在完成线程上同步执行(慎用)
- LongRunning:提示调度器这是长时间运行任务
1.3 调度器选择
正确的调度器选择对 WinForms 应用至关重要:
csharp复制// 使用UI线程调度器更新控件
Task.Factory.ContinueWhenAll(tasks, completedTasks => {
// 这个lambda会在UI线程执行
stockChart.Update(prices);
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
2. 实战:股票数据实时监控系统
让我们通过一个完整的股票监控案例,看看如何在实际项目中应用这些技术。
2.1 系统架构设计
我们的股票监控系统需要:
- 定时从多个数据源获取股票价格
- 合并结果显示在UI上
- 动态调整请求频率
- 处理各种异常情况
csharp复制public class StockMonitor : IDisposable
{
private readonly Timer _timer;
private readonly HttpClient _client;
private readonly ConcurrentDictionary<string, decimal> _prices = new();
private readonly SemaphoreSlim _throttler = new(3);
private readonly CancellationTokenSource _cts = new();
public StockMonitor(DataGridView grid)
{
_grid = grid;
_timer = new Timer { Interval = 1000 };
_timer.Tick += async (s, e) => await UpdatePricesAsync();
_client = new HttpClient();
}
public void Start() => _timer.Start();
public void Stop() => _cts.Cancel();
private async Task UpdatePricesAsync()
{
var symbols = new[] { "MSFT", "AAPL", "GOOG", "AMZN" };
var tasks = symbols.Select(s => GetPriceAsync(s)).ToArray();
await Task.Factory.ContinueWhenAll(tasks, completedTasks => {
var failed = completedTasks.Count(t => t.IsFaulted);
if (failed > 0) Log.Warning($"{failed} requests failed");
// 更新UI
_grid.Invoke(() => {
foreach (var task in completedTasks.Where(t => t.IsCompletedSuccessfully))
{
var (symbol, price) = task.Result;
_prices[symbol] = price;
}
_grid.DataSource = _prices.ToArray();
});
}, _cts.Token, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
private async Task<(string, decimal)> GetPriceAsync(string symbol)
{
await _throttler.WaitAsync(_cts.Token);
try {
var response = await _client.GetStringAsync(
$"https://api.example.com/stocks/{symbol}");
return (symbol, decimal.Parse(response));
}
finally {
_throttler.Release();
}
}
public void Dispose()
{
_cts?.Cancel();
_client?.Dispose();
_timer?.Dispose();
}
}
2.2 关键实现细节
- 并发控制:使用SemaphoreSlim限制同时进行的请求数
- 线程安全:ConcurrentDictionary保证价格更新的线程安全
- UI更新:通过TaskScheduler.FromCurrentSynchronizationContext确保UI操作在正确线程
- 异常处理:自动记录失败请求但不中断整个流程
- 资源清理:正确实现IDisposable接口
2.3 性能优化技巧
- 动态调整频率:根据响应时间自动调整轮询间隔
- 请求合并:对同一股票的多个订阅者合并请求
- 缓存策略:对变化不大的数据实施缓存
- 连接复用:保持HttpClient单例
csharp复制private async Task UpdatePricesAsync()
{
// 动态调整间隔
var sw = Stopwatch.StartNew();
// ...原有代码...
sw.Stop();
if (sw.ElapsedMilliseconds > 500)
{
_timer.Interval = Math.Min(5000, _timer.Interval + 200);
}
else if (_timer.Interval > 1000)
{
_timer.Interval = Math.Max(1000, _timer.Interval - 200);
}
}
3. 常见问题与解决方案
3.1 死锁问题
症状:UI冻结,应用无响应
原因:在UI线程上同步等待任务完成(.Result或.Wait)
解决方案:
csharp复制// 错误做法 - 可能导致死锁
var price = GetPriceAsync("MSFT").Result;
// 正确做法 - 使用await
var price = await GetPriceAsync("MSFT");
3.2 SemaphoreFullException
症状:信号量计数异常
原因:Release()调用次数多于Wait()
解决方案:
csharp复制// 使用using确保释放
await using (await _throttler.WaitAsync(_cts.Token))
{
// 执行操作
} // 自动释放
// 或者使用try-finally
try {
await _throttler.WaitAsync(_cts.Token);
// 执行操作
}
finally {
_throttler.Release();
}
3.3 任务取消
正确处理取消请求:
csharp复制private async Task GetPriceAsync(string symbol, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
await _throttler.WaitAsync(ct);
try {
var response = await _client.GetStringAsync(
$"https://api.example.com/stocks/{symbol}", ct);
return decimal.Parse(response);
}
catch (OperationCanceledException) {
Log.Info("Request canceled");
throw;
}
finally {
_throttler.Release();
}
}
4. 进阶应用场景
4.1 竞态请求处理
当有多个数据源时,使用ContinueWhenAny获取最快响应:
csharp复制public async Task<decimal> GetFastestStockPrice(string symbol)
{
var sources = new[] {
$"https://source1.com/{symbol}",
$"https://source2.com/{symbol}",
$"https://source3.com/{symbol}"
};
var tasks = sources.Select(s => FetchPriceAsync(s)).ToArray();
var completed = await Task.Factory.ContinueWhenAny(tasks, t => t);
return completed.Result;
async Task<decimal> FetchPriceAsync(string url)
{
var response = await _client.GetStringAsync(url);
return decimal.Parse(response);
}
}
4.2 批量数据处理管道
构建多阶段处理管道:
csharp复制public async Task ProcessDataBatchAsync(IEnumerable<string> data)
{
// 阶段1:并行处理原始数据
var stage1Tasks = data.Select(ProcessStage1Async).ToArray();
await Task.Factory.ContinueWhenAll(stage1Tasks, stage1Results => {
// 阶段2:合并中间结果
var stage2Tasks = stage1Results.Select(ProcessStage2Async).ToArray();
return Task.Factory.ContinueWhenAll(stage2Tasks, stage2Results => {
// 最终处理
return ProcessFinalResultsAsync(stage2Results);
});
});
}
4.3 与async/await模式结合
虽然ContinueWhenAll很有用,但在现代C#中,通常更推荐使用await Task.WhenAll:
csharp复制public async Task UpdateAllStocksAsync()
{
var symbols = new[] { "MSFT", "AAPL", "GOOG" };
var tasks = symbols.Select(GetStockAsync).ToArray();
// 更现代的写法
var stocks = await Task.WhenAll(tasks);
UpdateStockDisplay(stocks);
}
ContinueWhenAll的优势在于:
- 更细粒度的控制(TaskContinuationOptions)
- 可以指定特定的TaskScheduler
- 在复杂的并行模式中更灵活
5. 性能考量与最佳实践
5.1 避免过度并行化
虽然TPL让并行编程变得简单,但并不意味着越多线程越好。最佳实践包括:
- 对于I/O密集型任务,并行度控制在合理范围(通常CPU核心数的2-4倍)
- 对于CPU密集型任务,并行度不要超过CPU核心数
- 使用性能分析工具(如Visual Studio的性能分析器)监控线程使用情况
5.2 正确的异常处理
延续任务中的异常处理需要特别注意:
csharp复制try
{
await Task.Factory.ContinueWhenAll(tasks, completedTasks => {
var exceptions = completedTasks.Where(t => t.IsFaulted)
.Select(t => t.Exception);
if (exceptions.Any())
{
throw new AggregateException(exceptions);
}
// 正常处理
});
}
catch (AggregateException ae)
{
foreach (var e in ae.InnerExceptions)
{
Log.Error(e.Message);
}
}
5.3 资源清理模式
实现可靠的资源清理:
csharp复制public class ResourceIntensiveService : IDisposable, IAsyncDisposable
{
private readonly CancellationTokenSource _cts = new();
private readonly List<Task> _activeTasks = new();
private bool _disposed;
public Task ProcessAsync()
{
var task = Task.Factory.StartNew(async () => {
await DoWorkAsync(_cts.Token);
}, _cts.Token).Unwrap();
_activeTasks.Add(task);
return task;
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
_cts.Cancel();
try {
await Task.WhenAll(_activeTasks).WaitAsync(TimeSpan.FromSeconds(5));
}
catch (OperationCanceledException) { }
catch (TimeoutException) { }
_cts.Dispose();
}
public void Dispose() => DisposeAsync().AsTask().Wait();
}
6. 调试技巧与工具
6.1 Visual Studio 调试支持
- 使用"并行堆栈"窗口查看所有运行中的任务
- 使用"并行任务"窗口监控任务状态
- 设置条件断点检查特定任务状态
6.2 日志记录策略
有效的日志记录对调试异步代码至关重要:
csharp复制public static class TaskLogger
{
public static Task<T> WithLogging<T>(this Task<T> task, string operation)
{
return task.ContinueWith(t => {
if (t.IsFaulted)
{
Log.Error($"Operation {operation} failed: {t.Exception}");
}
else if (t.IsCanceled)
{
Log.Warning($"Operation {operation} was canceled");
}
return t.Result;
});
}
}
// 使用示例
var task = GetStockAsync("MSFT").WithLogging("Fetch MSFT price");
6.3 性能分析
使用BenchmarkDotNet对关键路径进行基准测试:
csharp复制[MemoryDiagnoser]
public class ContinuationBenchmarks
{
[Benchmark]
public async Task ContinueWhenAll()
{
var tasks = Enumerable.Range(1, 100)
.Select(i => Task.FromResult(i))
.ToArray();
await Task.Factory.ContinueWhenAll(tasks, _ => {});
}
[Benchmark]
public async Task WhenAll()
{
var tasks = Enumerable.Range(1, 100)
.Select(i => Task.FromResult(i))
.ToArray();
await Task.WhenAll(tasks);
}
}
7. 替代方案比较
7.1 ContinueWhenAll vs WhenAll
| 特性 | ContinueWhenAll | WhenAll |
|---|---|---|
| 调度控制 | 可指定TaskScheduler | 使用默认调度器 |
| 执行条件 | 可配置TaskContinuationOptions | 总是执行 |
| 异常处理 | 需要手动检查任务状态 | 自动聚合异常 |
| 代码简洁性 | 较复杂 | 更简洁 |
| UI线程访问 | 可指定UI调度器 | 需要额外配置 |
7.2 ContinueWhenAny vs WhenAny
| 特性 | ContinueWhenAny | WhenAny |
|---|---|---|
| 结果处理 | 在延续中处理 | 返回已完成的任务 |
| 使用模式 | 更适合响应式场景 | 更适合轮询式场景 |
| 组合灵活性 | 较难与其他任务组合 | 易于与其他任务组合 |
8. 实际项目经验分享
在多年的企业级应用开发中,我总结了以下宝贵经验:
- 超时策略:总是为关键操作设置超时
csharp复制var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await Task.Factory.ContinueWhenAll(tasks, _ => {},
cts.Token, TaskContinuationOptions.None, scheduler);
- 资源泄漏防护:使用using语句包装任务
csharp复制await using (var resource = new ExpensiveResource())
{
var task = resource.DoWorkAsync();
await Task.Factory.ContinueWhenAll(new[] { task }, _ => {});
}
- 上下文保持:在ASP.NET Core中注意同步上下文
csharp复制// 在ASP.NET Core中通常不需要同步上下文
var task = Task.Factory.StartNew(() => {
// 长时间运行的任务
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
- 取消令牌传播:始终传递取消令牌
csharp复制public async Task ProcessDataAsync(CancellationToken ct)
{
var tasks = _dataSources.Select(s => s.FetchAsync(ct)).ToArray();
await Task.Factory.ContinueWhenAll(tasks, _ => {}, ct);
}
- 错误恢复策略:实现重试逻辑
csharp复制public static async Task<T> WithRetry<T>(Func<Task<T>> operation, int retries)
{
while (true)
{
try {
return await operation();
}
catch when (retries-- > 0) {
await Task.Delay(1000);
}
}
}
9. 设计模式应用
9.1 管道模式
使用延续任务构建处理管道:
csharp复制public class ProcessingPipeline
{
public Task ProcessAsync(InputData input)
{
return ValidateInputAsync(input)
.ContinueWith(t => TransformDataAsync(t.Result))
.Unwrap()
.ContinueWith(t => SaveResultAsync(t.Result))
.Unwrap();
}
private Task<ValidatedData> ValidateInputAsync(InputData input) { ... }
private Task<TransformedData> TransformDataAsync(ValidatedData data) { ... }
private Task SaveResultAsync(TransformedData data) { ... }
}
9.2 扇出/扇入模式
并行处理多个项目后合并结果:
csharp复制public async Task<Result> ProcessInParallelAsync(IEnumerable<Item> items)
{
var tasks = items.Select(ProcessItemAsync).ToArray();
await Task.Factory.ContinueWhenAll(tasks, completedTasks => {
var failed = completedTasks.Count(t => t.IsFaulted);
if (failed > 0) throw new Exception($"{failed} items failed");
});
return CombineResults(tasks.Select(t => t.Result));
}
9.3 生产者/消费者模式
使用延续任务协调生产者和消费者:
csharp复制public class ProducerConsumer
{
private readonly BlockingCollection<WorkItem> _queue = new();
public void Start(int consumerCount)
{
var consumers = Enumerable.Range(0, consumerCount)
.Select(_ => Task.Run(ConsumeAsync))
.ToArray();
Task.Factory.ContinueWhenAll(consumers, _ => {
Log.Info("All consumers completed");
});
}
private async Task ConsumeAsync()
{
foreach (var item in _queue.GetConsumingEnumerable())
{
await ProcessItemAsync(item);
}
}
}
10. 未来发展与替代方案
虽然ContinueWhenAll/ContinueWhenAny仍然有效,但现代C#开发中,以下替代方案也值得考虑:
- System.Threading.Channels:用于高效的生产者/消费者场景
- Dataflow (TPL Dataflow Library):提供更强大的数据流编程模型
- IAsyncEnumerable:C# 8.0引入的异步流
- Parallel.ForEachAsync:.NET 6引入的异步并行循环
csharp复制// 使用Parallel.ForEachAsync (.NET 6+)
await Parallel.ForEachAsync(items, async (item, ct) => {
await ProcessItemAsync(item, ct);
});
// 使用System.Threading.Channels
var channel = Channel.CreateBounded<WorkItem>(100);
var writer = channel.Writer;
var reader = channel.Reader;
// 生产者
await writer.WriteAsync(new WorkItem());
// 消费者
await foreach (var item in reader.ReadAllAsync())
{
await ProcessItemAsync(item);
}
选择哪种技术取决于具体场景:
- 简单任务延续:ContinueWhenAll/ContinueWhenAny
- 复杂数据流:TPL Dataflow
- 高性能生产者/消费者:Channels
- 批量数据处理:Parallel.ForEachAsync