不错的主题 👍 ——“献丑贴: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.ForEachParallel.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🚀 最优最稳定

九、经验总结 🧩

  1. Task.Run 只是任务启动,不等于并发优化
  2. 控制并发量,是关键。
  3. .NET 6+ 下优先使用 Parallel.ForEachAsync
  4. 异步 IO 不要滥用 Task.Run。
  5. CPU 密集型任务适合 Parallel.ForEach
  6. 若任务数量巨大,用分批(Chunk)+ 控制并发。

🔚 十、示例总结

最终推荐写法:

await Parallel.ForEachAsync(items, new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount
}, async (item, token) =>
{
    await DoWorkAsync(item);
});