线程 vs 任务 vs 并行 — 完整的 .NET 并发指南
I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? Once I have the text, I’ll provide a Simplified Chinese translation while preserving the original formatting, markdown, and any code blocks.
线程、ThreadPool、Task、Parallel.For、PLINQ – 何时使用它们
并发是 .NET 开发中最常被误解的领域之一。
Thread、Task、Parallel、async/await —— 开发者常常把它们混用,却不了解它们的实际作用。错误的选择会导致性能、正确性和可扩展性受损。
注意: 另一篇文章 Async/Await in C# — A Deep Dive Into How Asynchronous Programming Really Works 简要介绍了异步和并行的区别。本指南则更深入地探讨底层原语 —— Thread、ThreadPool、Task 和 Parallel。
心智模型
- 并发 – 同时处理多件事(管理)
- 并行 – 同时执行多件事(执行)
- 异步 – 启动某事并在等待时做其他工作
这些并不是同一件事。
线程 — 低层原语
var thread = new Thread(() =>
{
Console.WriteLine($"Running on thread {Thread.CurrentThread.ManagedThreadId}");
});
thread.Start();
thread.Join(); // Wait for it to finish- 完全控制线程生命周期(优先级、名称、公寓状态)
- 可以是前台或后台
- 每个线程分配约 1 MB 的栈内存
- 每次上下文切换都有操作系统调度开销
- 需要自行管理生命周期
- 不 与
async/await集成
何时使用: 在现代 .NET 中几乎从不使用。唯一剩余的有效用例是需要特定公寓状态的 COM 互操作(WinForms/WPF 对话框的 STA 线程)。
线程池 — 重用线程
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine("Running on a pool thread");
});- 无需显式创建线程——只需提交工作项,由线程池决定使用哪条线程来处理。
- 没有线程创建的开销;线程在不同工作项之间被复用。
- 运行时会自动调节线程池的大小。
Task.Run() 包装了线程池,并提供了更好的 API。
任务 — 现代标准
// CPU‑bound work on the ThreadPool
var task = Task.Run(() => ExpensiveCalculation());
var result = await task;- Task 是一个承诺,表示工作将在某个时刻完成。
Task– 没有返回值的工作Task<T>– 返回值的工作ValueTask– 用于高频路径的轻量级任务
// Run two tasks in parallel and wait for both
var t1 = Task.Run(() => DoWork1());
var t2 = Task.Run(() => DoWork2());
await Task.WhenAll(t1, t2);
// Wait for the first one to finish
var winner = await Task.WhenAny(t1, t2);async/await — 异步而不阻塞
async/await 不是 用于并行;它在等待 I/O(网络调用、数据库查询、文件读取)时释放线程。
// The thread is released while waiting for the HTTP response
var response = await httpClient.GetAsync(url);常见场景
| 场景 | 推荐的 API |
|---|---|
| 等待数据库查询 | async/await |
| 等待 HTTP 调用 | async/await |
| 运行耗时计算 | Task.Run() |
| 运行多个计算 | Parallel.For / Task.WhenAll |
Parallel — 真正的 CPU 并行
Parallel.For(0, 1000, i => ProcessItem(i));var items = GetLargeCollection();
Parallel.ForEach(items, item => Process(item));var results = items
.AsParallel()
.Where(x => x.IsValid)
.Select(x => Transform(x))
.ToList();- 将工作分配到多个 CPU 核心同时执行。
- 仅用于可以划分为独立块的 CPU 密集型工作。
- 绝不要将
Parallel用于 I/O 密集型工作——这会浪费线程并可能导致线程饥饿。
决策框架
Is the work CPU‑bound or I/O‑bound?
│
├── I/O‑bound (network, DB, file)
│ └── Use async/await
│ └── Task.Run is NOT needed here
│
└── CPU‑bound (calculations, image processing)
│
├── Single heavy operation
│ └── Task.Run(() => HeavyWork())
│
└── Many independent items
└── Parallel.For / Parallel.ForEach / PLINQ常见错误
错误 1:对 I/O 绑定工作使用 Task.Run
// ❌ Wrong — wastes a thread waiting for I/O
var result = await Task.Run(() => httpClient.GetAsync(url));
// ✅ Correct — no thread needed while waiting
var result = await httpClient.GetAsync(url);错误 2:把 CPU 工作伪装成异步
// ❌ Looks async but blocks the thread
public async Task ComputeAsync()
{
return HeavyCpuWork(); // No await — just pretending to be async
}
// ✅ Offload to ThreadPool
public async Task ComputeAsync()
{
return await Task.Run(() => HeavyCpuWork());
}错误 3:在 Parallel.ForEach 中对 I/O 进行阻塞
// ❌ Spawns threads and blocks them all waiting for HTTP
Parallel.ForEach(urls, url =>
{
var result = httpClient.GetAsync(url).Result; // Blocking!
});
// ✅ Use Task.WhenAll for parallel async I/O
var tasks = urls.Select(url => httpClient.GetAsync(url));
var results = await Task.WhenAll(tasks);面试准备摘要
- Thread – 操作系统级别的原语;功能强大但开销大,通常不需要直接使用。
- ThreadPool – 复用线程;
Task.Run对其进行封装。 - Task – 用于并发工作的标准抽象;支持
async/await。 - async/await – 在 I/O 操作期间释放线程;不是并行。
- Parallel.For / Parallel.ForEach / PLINQ – 将 CPU 工作分配到多个核心。
指导原则:
- I/O 密集型 →
async/await(不使用Task.Run)。 - CPU 密集型,单个重操作 →
Task.Run。 - CPU 密集型,多个独立项 →
Parallel.For/Parallel.ForEach/ PLINQ。
切勿将 Parallel 用于 I/O 密集型工作——这会浪费线程。
简洁的面试回答
“Thread 是操作系统层面的执行单元——创建成本高。Task 抽象了 ThreadPool 并支持
async/await。async/await的目的是在 I/O 期间释放线程,而不是并行。并行指的是在多个核心上同时实际执行工作——这正是Parallel.For和Task.WhenAll所做的。黄金法则:对 I/O 密集型工作使用async/await,对单个 CPU 密集型操作使用Task.Run,对大量 CPU 密集型操作使用Parallel(或 PLINQ)。”