前言

在 .NET 8 里,多线程和并发的玩法比早期丰富了很多。从最原始的 Thread,到线程池,再到 TaskParallelasync/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 密集型的数据并行,比如批处理、图像处理、大批量计算。

优点:自动分块、支持 MaxDegreeOfParallelismParallelLoopState 支持提前 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 密集 + 数据批处理ParallelTask.Run + Parallel.For
  • IO 密集 + 不阻塞async/await + Task.WhenAll
  • 生产者-消费者 / 流水线Channel<T>
  • 需要返回值、取消、组合Task
  • 极端控制需求Thread(很少用到)
  • 临时丢个小任务ThreadPool.QueueUserWorkItem

实际项目里 90% 的场景,Task + async/await + Channel 这三件套就够用了。Thread 基本只在历史代码或特殊需求里出现,Parallel 在数据并行时偶有登场。

小结

.NET 8 的多线程生态已经非常成熟:从底层的 ThreadThreadPool,到中层的 TaskParallel,再到上层的 async/awaitChannel,每一层都解决了上一层的痛点。理解它们各自的定位,才能在合适的场景选对工具,写出既快又好维护的并发代码。

下次遇到并发需求时,先问自己一句:这是 CPU 密集还是 IO 密集?是单次任务还是流水线?答案出来,选型也就出来了。