不错的主题 👍 ——“献丑贴:Task.Run 中 foreach 优化” 其实是一个很经典、实用的 .NET 并发优化话题。下面我给你整理一个系统性讲解 + 实战优化案例,可以直接作为笔记、技术博客或分享贴使用。
🧠 献丑贴:Task.Run 中 foreach 优化实践笔记
一、背景:异步并发 ≠ 真并行
很多人初学 C# 异步时,会写出这样的代码:
foreach (var item in items)
{
Task.Run(() => DoWork(item));
}
看起来每个任务都异步执行,但实际上:
- 这些任务确实是异步的;
- 但控制力极弱:没有等待、没有并发限制、容易资源竞争;
- 一旦
items
很大,CPU、IO、线程池都可能炸。
二、常见问题 ⚠️
问题 | 说明 |
---|---|
线程爆炸 | 如果有成千上万个元素,每个都 Task.Run,会导致 ThreadPool 频繁切换。 |
缺乏等待 | foreach 外层不会等待所有任务结束,可能方法提前返回。 |
异常丢失 | 内部异常被吞掉,外层 catch 不到。 |
并发无控制 | 任务之间争抢 CPU / IO,性能反而下降。 |
三、第一步优化:收集任务 + Task.WhenAll
最基本的改进方法是——等待所有任务执行完:
var tasks = new List<Task>();
foreach (var item in items)
{
tasks.Add(Task.Run(() => DoWork(item)));
}
await Task.WhenAll(tasks);
✅ 优点:简单可靠
❌ 缺点:没有并发限制,大数据仍会压垮系统。
四、第二步优化:并发限制 —— 使用 SemaphoreSlim
如果你需要限制同时执行的任务数量,比如最多 5 个并发:
var semaphore = new SemaphoreSlim(5); // 限制并发数为5
var tasks = items.Select(async item =>
{
await semaphore.WaitAsync();
try
{
await Task.Run(() => DoWork(item));
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
✅ 优点:
- 控制并发量;
- 避免资源被同时抢占;
- 支持真正的异步等待。
五、第三步优化:使用 Parallel.ForEachAsync
(.NET 6+ 推荐🔥)
如果你使用的是 .NET 6 或更高版本,可以直接用:
await Parallel.ForEachAsync(items, new ParallelOptions
{
MaxDegreeOfParallelism = 5
}, async (item, token) =>
{
await DoWorkAsync(item);
});
✅ 优点:
- 内置并发控制;
- 自动等待;
- 性能优于手动 Task.Run;
- 支持取消令牌;
- 无需额外集合管理。
六、第四步优化:区分 CPU 任务与 IO 任务
类型 | 建议方式 | 原因 |
---|---|---|
CPU 密集型 | 用 Parallel.ForEach 或 Parallel.ForEachAsync | 利用多核并行提升效率 |
IO 密集型(如文件下载、网络请求) | 用 SemaphoreSlim 控制异步请求 | 避免线程池饥饿 |
示例(IO 场景):
var semaphore = new SemaphoreSlim(10);
var tasks = new List<Task>();
foreach (var url in urls)
{
await semaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
{
try
{
await DownloadFileAsync(url);
}
finally
{
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
七、第五步优化:批量处理(Chunk 分组)
如果 items
数量特别大(如 10 万),推荐分批处理:
const int batchSize = 500;
foreach (var batch in items.Chunk(batchSize))
{
await Parallel.ForEachAsync(batch, async (item, token) =>
{
await DoWorkAsync(item);
});
}
✅ 避免一次性创建过多任务
✅ 减少内存与调度压力
八、性能对比示意(1000 个任务)
方案 | 平均耗时 | 内存占用 | 稳定性 |
---|---|---|---|
原始 Task.Run foreach | ❌ 慢、随机 | 高 | 不稳定 |
Task.WhenAll | ✅ 中等 | 中 | 稳定 |
SemaphoreSlim 控制 | ✅ 快 | 低 | 稳定 |
Parallel.ForEachAsync | 🚀 最优 | 低 | 最稳定 |
九、经验总结 🧩
- Task.Run 只是任务启动,不等于并发优化。
- 控制并发量,是关键。
.NET 6+
下优先使用Parallel.ForEachAsync
。- 异步 IO 不要滥用 Task.Run。
- CPU 密集型任务适合
Parallel.ForEach
。 - 若任务数量巨大,用分批(Chunk)+ 控制并发。
🔚 十、示例总结
最终推荐写法:
await Parallel.ForEachAsync(items, new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount
}, async (item, token) =>
{
await DoWorkAsync(item);
});
发表回复