线程 vs 任务 vs 并行 — 完整的 .NET 并发指南

发布: (2026年4月11日 GMT+8 22:00)
7 分钟阅读
原文: Dev.to

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/awaitasync/await 的目的是在 I/O 期间释放线程,而不是并行。并行指的是在多个核心上同时实际执行工作——这正是 Parallel.ForTask.WhenAll 所做的。黄金法则:对 I/O 密集型工作使用 async/await,对单个 CPU 密集型操作使用 Task.Run,对大量 CPU 密集型操作使用 Parallel(或 PLINQ)。”

0 浏览
Back to Blog

相关文章

阅读更多 »