前言
在 .NET 8 里,多线程和并发的玩法比早期丰富了很多。从最原始的 Thread,到线程池,再到 Task、Parallel、async/await,以及更现代的 Channel,每一种都有自己擅长的场景。
本篇就用通俗的语言 + 可运行的代码,把这几种方式梳理一遍,最后给出一张对比表,帮你选型不迷路。
一、Thread 类:最原始的线程
Thread 是 .NET 里最底层的线程 API,直接对应操作系统的线程。创建一个 Thread 就是新建一个 OS 线程,开销不小(默认 1MB 栈空间)。
// Thread:显式创建一个线程
var thread = new Thread(() =>
{
// 新线程里执行的工作
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Thread 工作中: {i}");
Thread.Sleep(100); // 模拟耗时
}
});
thread.IsBackground = true; // 设为后台线程,主线程退出时自动结束
thread.Start(); // 启动线程
thread.Join(); // 等待线程完成
Console.WriteLine("Thread 完成");
适用场景:需要长时间运行的后台任务、需要精确控制线程优先级或栈大小的特殊场景。
优点:控制力最强,能设置优先级、前台/后台、栈大小。
缺点:创建销毁开销大、不自带线程池复用、异常处理麻烦、API 偏老。
二、ThreadPool:线程池复用
每次新建 Thread 太贵,于是有了线程池。把任务丢给线程池,由它从池子里挑一个空闲线程来执行,省去了频繁创建销毁的开销。
// ThreadPool:把任务排入线程池队列
ThreadPool.QueueUserWorkItem(state =>
{
// 线程池里的某个线程会执行这段代码
Console.WriteLine($"线程池线程执行,参数: {state}");
Thread.Sleep(200);
}, "hello");
// 主线程也做点事,避免程序提前退出
Thread.Sleep(500);
Console.WriteLine("主线程结束");
.NET 8 还可以用 ThreadPool.UnsafeQueueUserWorkItem 跳过 ExecutionContext 捕获,性能更好(但会丢失异步上下文流转)。
适用场景:短小的、不需要返回值、不需要精细控制的后台任务。
优点:线程复用、开销小、自动管理并发数。
缺点:不能直接拿返回值、不能直接 cancel、排队顺序不保证、调试困难。
三、Task:现代异步的基石
Task 是 TPL(任务并行库)的核心,从 .NET 4.0 引入至今仍是主力。它本质上是”一段工作的 Promise”,背后跑在线程池上,但支持返回值、延续、取消、异常聚合。
// Task:带返回值的异步任务
Task<int> task = Task.Run(() =>
{
// 在线程池线程上计算
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
return sum;
});
// 注册延续:任务完成后执行
task.ContinueWith(t =>
{
Console.WriteLine($"计算结果: {t.Result}");
}, TaskContinuationOptions.OnlyOnRanToCompletion);
task.Wait(); // 实际项目里用 await,这里演示用
带取消支持的写法:
// Task + CancellationToken:可取消的任务
using var cts = new CancellationTokenSource();
var cancelableTask = Task.Run(() =>
{
for (int i = 0; i < 1000; i++)
{
cts.Token.ThrowIfCancellationRequested(); // 检查取消
Thread.Sleep(10);
}
return "完成";
}, cts.Token);
// 1秒后取消
cts.CancelAfter(TimeSpan.FromSeconds(1));
try
{
var result = await cancelableTask;
Console.WriteLine(result);
}
catch (OperationCanceledException)
{
Console.WriteLine("任务被取消");
}
适用场景:绝大多数异步/并行场景,需要返回值、取消、异常处理的首选。
优点:API 丰富、支持返回值、可组合(WhenAll/WhenAny)、与 async/await 无缝衔接。
缺点:背后仍是线程池,对超长时间任务不友好(会占用池线程);过度嵌套 ContinueWith 会变回调地狱。
四、Parallel:数据/任务并行
Parallel 专门用于”对一批数据做相同操作”的场景,比如循环并行化。它内部用 Partitioner 把数据分块,自动管理并发度。
// Parallel.For:并行循环
int[] data = Enumerable.Range(1, 1000).ToArray();
// 并行处理 1~1000,内部自动分块调度
Parallel.For(0, data.Length, i =>
{
data[i] = data[i] * data[i]; // 求平方
});
Console.WriteLine($"前5个结果: {string.Join(",", data.Take(5))}");
// Parallel.ForEach:并行遍历集合
var list = new List<string>();
var lockObj = new object();
Parallel.ForEach(Enumerable.Range(1, 10), item =>
{
lock (lockObj) // 多线程写集合需要加锁
{
list.Add($"item-{item}");
}
});
// Parallel.ForEach 也可以用 Partitioner 提升分块效率
Parallel.ForEach(Partitioner.Create(0, 1000), range =>
{
for (int i = range.Item1; i < range.Item2; i++)
{
// 处理 [range.Item1, range.Item2) 区间
}
});
控制并发度:
// 限制最大并行度
var options = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount // 最多用 CPU 核数个线程
};
Parallel.For(0, 100, options, i =>
{
// 最多 8(取决于CPU)个线程同时跑
});
适用场景:CPU 密集型的数据并行,比如批处理、图像处理、大批量计算。
优点:自动分块、支持 MaxDegreeOfParallelism、ParallelLoopState 支持提前 Break/Stop。
缺点:阻塞调用线程、不适合 IO 密集型(会占着线程池线程等 IO)、异常处理要用 AggregateException。
五、async/await:异步编程的甜点
严格说 async/await 不是”多线程”,而是异步编程模型。它让你用同步的写法写异步代码,编译器会生成状态机。IO 密集型任务(网络、磁盘、数据库)用它最合适,因为等待 IO 时线程会被释放回线程池,不浪费资源。
// async/await:异步下载网页
async Task<string> DownloadAsync(string url, CancellationToken ct)
{
using var http = new HttpClient();
// await 时当前线程被释放,不阻塞
var resp = await http.GetAsync(url, ct);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsStringAsync(ct);
}
// 并发发起多个请求
var urls = new[] { "https://a.com", "https://b.com", "https://c.com" };
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// WhenAll:等所有任务完成
var tasks = urls.Select(u => DownloadAsync(u, cts.Token));
string[] results = await Task.WhenAll(tasks);
foreach (var html in results)
{
Console.WriteLine($"下载长度: {html.Length}");
}
适用场景:IO 密集型(HTTP、数据库、文件)、UI 程序保持响应、高并发服务端。
优点:不阻塞线程、写法接近同步、可读性好、天然支持取消和异常传播。
缺点:本质是单线程切换上下文,不适合 CPU 密集型;要小心 .Result / .Wait() 死锁;async void 是坑。
六、Channel:生产者-消费者管道
Channel<T> 是 .NET Core 3.0 引入、在 .NET 8 里继续强化的高性能异步队列,专门为生产者-消费者模型设计。比 ConcurrentQueue + BlockingCollection 更现代、性能更好。
using System.Threading.Channels;
// 创建一个有界 Channel:最多缓存 100 条
var channel = Channel.CreateBounded<int>(100);
// 生产者:往 Channel 写数据
async Task ProduceAsync(ChannelWriter<int> writer, CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
await writer.WriteAsync(i, ct); // 满了会异步等待
Console.WriteLine($"生产: {i}");
}
writer.Complete(); // 通知消费者:没有更多数据了
}
// 消费者:从 Channel 读数据
async Task ConsumeAsync(ChannelReader<int> reader, CancellationToken ct)
{
await foreach (var item in reader.ReadAllAsync(ct)) // 流式读取
{
Console.WriteLine($"消费: {item}");
await Task.Delay(10, ct); // 模拟处理耗时
}
Console.WriteLine("消费结束");
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
// 启动 1 个生产者 + 2 个消费者
var producer = ProduceAsync(channel.Writer, cts.Token);
var consumer1 = ConsumeAsync(channel.Reader, cts.Token);
var consumer2 = ConsumeAsync(channel.Reader, cts.Token);
await Task.WhenAll(producer, consumer1, consumer2);
Console.WriteLine("全部完成");
适用场景:流水线处理、消息队列、限流消费、后台任务队列。
优点:纯异步、高性能、支持有界/无界、背压(Bounded 满了会 await)、API 简洁。
缺点:只解决”数据传递”,不解决”如何产生数据”;学习成本比 BlockingCollection 略高。
七、对比总结
| 方式 | 本质 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| Thread | OS 线程 | 长时间后台任务、需精细控制 | 控制力最强 | 开销大、API 老、不池化 |
| ThreadPool | 线程池复用 | 短小无返回值任务 | 开销小、自动管理 | 无返回值、不可取消 |
| Task | TPL 任务 | 通用异步/并行 | 支持返回值/取消/组合 | 长任务占池线程 |
| Parallel | 数据并行 | CPU 密集批处理 | 自动分块、可限流 | 阻塞、不适合 IO |
| async/await | 异步状态机 | IO 密集、高并发 | 不阻塞、可读性好 | 不适合 CPU 密集 |
| Channel | 异步队列 | 生产者-消费者 | 高性能、背压、流式 | 只管数据传递 |
选型建议
记一个简单的口诀:
- CPU 密集 + 数据批处理 →
Parallel或Task.Run+Parallel.For - IO 密集 + 不阻塞 →
async/await+Task.WhenAll - 生产者-消费者 / 流水线 →
Channel<T> - 需要返回值、取消、组合 →
Task - 极端控制需求 →
Thread(很少用到) - 临时丢个小任务 →
ThreadPool.QueueUserWorkItem
实际项目里 90% 的场景,Task + async/await + Channel 这三件套就够用了。Thread 基本只在历史代码或特殊需求里出现,Parallel 在数据并行时偶有登场。
小结
.NET 8 的多线程生态已经非常成熟:从底层的 Thread、ThreadPool,到中层的 Task、Parallel,再到上层的 async/await、Channel,每一层都解决了上一层的痛点。理解它们各自的定位,才能在合适的场景选对工具,写出既快又好维护的并发代码。
下次遇到并发需求时,先问自己一句:这是 CPU 密集还是 IO 密集?是单次任务还是流水线?答案出来,选型也就出来了。