c# .NET core多线程的详细讲解
在 .NET Core 中,多线程是实现并发执行任务的核心技术,可有效提升程序对 CPU 和 I/O 资源的利用率(如同时处理多个网络请求、并行计算等)。.NET Core 提供了丰富的多线程 API,从基础的 Thread
类到现代的 Task
/async/await
,再到并行计算库,覆盖了不同场景的需求。本文将从概念、核心 API、线程安全到实际应用,详细讲解 .NET Core 多线程编程。
一、多线程基础概念
- 线程(Thread):操作系统调度的最小单位,负责执行程序中的指令。一个进程(如一个 .NET 程序)可包含多个线程,共享进程的内存空间,但拥有独立的栈和寄存器。
- 并发(Concurrency):多个任务在同一时间段内交替执行(如单 CPU 核心通过快速切换线程实现“同时”执行)。
- 并行(Parallelism):多个任务在同一时刻真正同时执行(依赖多 CPU 核心,每个核心执行一个线程)。
- 多线程的目的:
- 提高 CPU 利用率(尤其 CPU 密集型任务,如数据分析、复杂计算);
- 避免单线程阻塞(尤其 I/O 密集型任务,如文件读写、网络请求时,线程无需等待 I/O 完成)。
二、.NET Core 多线程核心 API
.NET Core 提供了多层次的多线程 API,从底层的线程控制到高层的任务抽象,选择需结合场景(如任务类型、复杂度、是否需要返回值等)。
1. System.Threading.Thread
:基础线程控制
Thread
类是最底层的线程 API,直接操作操作系统线程,适合需要细粒度控制线程(如优先级、前台/后台线程)的场景。
基本用法
using System;
using System.Threading;class ThreadDemo
{static void Main(){// 1. 创建线程:传入线程执行的方法(无参数)Thread thread1 = new Thread(PrintNumbers);// 2. 启动线程thread1.Start();// 3. 创建带参数的线程(参数必须是 object 类型)Thread thread2 = new Thread(PrintMessage);thread2.Start("Hello from thread2"); // 传递参数// 主线程执行Console.WriteLine("Main thread is running...");// 4. 等待线程执行完成(阻塞主线程)thread1.Join(); // 等待 thread1 结束thread2.Join(); // 等待 thread2 结束Console.WriteLine("All threads completed.");}// 线程执行的方法(无参数)static void PrintNumbers(){for (int i = 1; i <= 5; i++){Console.WriteLine($"Thread1: {i}");Thread.Sleep(100); // 模拟工作(暂停 100ms,释放 CPU 给其他线程)}}// 线程执行的方法(带参数)static void PrintMessage(object message){string msg = message as string ?? "No message";for (int i = 1; i <= 3; i++){Console.WriteLine($"Thread2: {msg} - {i}");Thread.Sleep(150);}}
}
输出(顺序可能因线程调度不同而变化):
Main thread is running...
Thread1: 1
Thread2: Hello from thread2 - 1
Thread1: 2
Thread2: Hello from thread2 - 2
Thread1: 3
Thread1: 4
Thread2: Hello from thread2 - 3
Thread1: 5
All threads completed.
关键属性与方法
IsBackground
:设置为true
时,线程为后台线程(进程退出时自动终止,如控制台程序主线程结束后,后台线程会被强制终止);默认false
为前台线程(进程需等待所有前台线程结束才退出)。thread1.IsBackground = true; // 设为后台线程
Priority
:设置线程优先级(Lowest
/BelowNormal
/Normal
/AboveNormal
/Highest
),操作系统会优先调度高优先级线程(但不保证,受系统调度算法影响)。Abort()
:终止线程(已过时,可能导致资源泄漏,建议通过CancellationToken
优雅终止)。Join(int millisecondsTimeout)
:等待线程完成,最多等待指定毫秒数。
优缺点
- 优点:直接控制线程,适合需要设置优先级、前台/后台属性的场景。
- 缺点:
- 线程创建/销毁开销大(每个线程默认栈大小 1MB,且操作系统调度成本高);
- 不适合大量短期任务(如同时处理 1000 个请求,创建 1000 个线程会耗尽资源);
- 无内置返回值支持(需手动通过共享变量获取结果)。
2. ThreadPool
:线程池管理
线程池是 .NET 维护的线程集合,用于复用线程(避免频繁创建/销毁),适合大量短期任务(如 Web 服务器处理请求、小计算任务)。线程池自动管理线程数量(默认最小 1 个,最大根据 CPU 核心动态调整)。
基本用法
using System;
using System.Threading;class ThreadPoolDemo
{static void Main(){// 1. 向线程池提交任务(无返回值)ThreadPool.QueueUserWorkItem(PrintNumbers); // 无参数// 2. 提交带参数的任务ThreadPool.QueueUserWorkItem(PrintMessage, "Hello from ThreadPool"); // 参数作为第二个参数// 主线程等待(避免进程提前退出,因为线程池线程是后台线程)Console.WriteLine("Main thread waiting...");Thread.Sleep(2000); // 等待足够长时间让线程池任务执行Console.WriteLine("Main thread exit.");}static void PrintNumbers(object? state) // 线程池任务方法必须接受 object? 参数{for (int i = 1; i <= 5; i++){Console.WriteLine($"ThreadPool Task1: {i}");Thread.Sleep(100);}}static void PrintMessage(object? state){string msg = state as string ?? "No message";for (int i = 1; i <= 3; i++){Console.WriteLine($"ThreadPool Task2: {msg} - {i}");Thread.Sleep(150);}}
}
关键特性
- 后台线程:线程池中的线程都是后台线程,进程退出时会被终止。
- 线程复用:任务完成后,线程不会销毁,而是回到线程池等待新任务,减少开销。
- 最大线程数控制:可通过
ThreadPool.SetMaxThreads
调整最大工作线程数和 I/O 完成端口线程数(通常无需手动调整,.NET 会自动优化)。// 获取当前线程池设置 ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads); Console.WriteLine($"Max worker threads: {workerThreads}, Max completion threads: {completionPortThreads}");// 设置最大工作线程数(示例:设为 100) ThreadPool.SetMaxThreads(100, completionPortThreads);
优缺点
- 优点:减少线程创建开销,适合大量短期任务,自动管理线程数量。
- 缺点:
- 无法设置线程优先级(所有线程池线程优先级为
Normal
); - 无法直接获取任务返回值(需手动处理);
- 不适合长时间运行的任务(会占用线程池资源,导致其他任务等待)。
- 无法设置线程优先级(所有线程池线程优先级为
3. Task
与 Task<TResult>
:基于任务的异步编程(TAP)
Task
是 .NET Core 推荐的现代异步编程模型(TAP:Task-based Asynchronous Pattern),抽象了线程的细节,专注于“任务”的执行和结果。Task
无返回值,Task<TResult>
有返回值,可通过线程池或单独线程执行。
基本用法(创建与等待任务)
using System;
using System.Threading.Tasks;class TaskDemo
{static async Task Main() // 注意:Main 方法可声明为 async Task{// 1. 创建并启动任务(无返回值):Task.Run 会将任务放入线程池Task task1 = Task.Run(() => {for (int i = 1; i <= 5; i++){Console.WriteLine($"Task1: {i}");Task.Delay(100).Wait(); // 模拟工作(类似 Thread.Sleep,但返回 Task)}});// 2. 创建带返回值的任务(Task<int>)Task<int> task2 = Task.Run(() => {int sum = 0;for (int i = 1; i <= 5; i++){sum += i;Console.WriteLine($"Task2: Adding {i}, sum={sum}");Task.Delay(150).Wait();}return sum; // 返回结果});// 3. 等待任务完成(多种方式)// 3.1 同步等待(阻塞当前线程)task1.Wait(); // 等待 task1 完成// 3.2 异步等待(不阻塞当前线程,释放 CPU 给其他任务)int result = await task2; // await 是关键:当前方法暂停,等待 task2 完成后再继续Console.WriteLine($"Task2 结果:{result}"); // 输出:15(1+2+3+4+5)// 4. 同时等待多个任务Task task3 = Task.Run(() => Console.WriteLine("Task3 完成"));Task task4 = Task.Run(() => Console.WriteLine("Task4 完成"));await Task.WhenAll(task3, task4); // 等待所有任务完成Console.WriteLine("Task3 和 Task4 都已完成");}
}
async/await
语法(核心)
async/await
是简化 Task
编程的语法糖,让异步代码看起来像同步代码,避免“回调地狱”。
async
:标记方法为异步方法,返回类型必须是void
(不推荐)、Task
或Task<TResult>
。await
:暂停当前方法执行,等待Task
完成后再继续,期间不阻塞线程(线程可去执行其他任务)。
示例:I/O 密集型任务(文件读写)
using System;
using System.IO;
using System.Threading.Tasks;class AsyncAwaitDemo
{static async Task Main(){Console.WriteLine("开始读取文件...");string content = await ReadFileAsync("test.txt"); // 异步等待文件读取完成Console.WriteLine($"文件内容:{content}");}// 异步读取文件(I/O 密集型任务,适合用 async/await)static async Task<string> ReadFileAsync(string path){// 使用 File.ReadAllTextAsync(返回 Task<string>),并用 await 等待return await File.ReadAllTextAsync(path); // 不阻塞线程,I/O 完成后自动回调}
}
为什么 I/O 密集型任务适合 async/await
?
I/O 操作(如文件读写、网络请求)时,CPU 处于空闲状态(等待硬件响应)。async/await
会释放当前线程去处理其他任务,I/O 完成后再通过线程池线程继续执行后续代码,大幅提高线程利用率。
任务延续(ContinueWith
)
任务完成后自动执行后续操作(无需显式等待):
Task<int> task = Task.Run(() =>
{Console.WriteLine("任务执行中...");return 100;
});// 任务完成后执行延续操作
task.ContinueWith(t =>
{Console.WriteLine($"任务结果:{t.Result},延续操作执行");
});// 等待所有操作完成
task.Wait();
优缺点
- 优点:
- 抽象线程细节,专注任务逻辑;
- 支持返回值、异常处理、任务延续;
async/await
简化异步代码,避免回调嵌套;- 高效利用线程(尤其 I/O 密集型任务)。
- 缺点:学习曲线较陡,需理解任务状态(
Running
/RanToCompletion
/Faulted
等)。
4. Parallel
类:数据并行计算
Parallel
类用于数据并行(将数据分成多个部分,并行处理),适合 CPU 密集型任务(如大规模数据计算),自动利用多核 CPU 提高效率。
基本用法(Parallel.For
和 Parallel.ForEach
)
using System;
using System.Threading.Tasks;class ParallelDemo
{static void Main(){// 1. Parallel.For:并行循环(类似 for 循环)Console.WriteLine("Parallel.For 开始...");Parallel.For(0, 10, i => // 从 0 到 10(不包含 10),并行执行每个 i{Console.WriteLine($"For 循环:i={i},线程 ID={Task.CurrentId}");Task.Delay(100).Wait(); // 模拟计算});Console.WriteLine("Parallel.For 结束\n");// 2. Parallel.ForEach:并行遍历集合(类似 foreach)int[] numbers = { 1, 2, 3, 4, 5 };Console.WriteLine("Parallel.ForEach 开始...");Parallel.ForEach(numbers, num => // 并行处理 numbers 中的每个元素{int square = num * num;Console.WriteLine($"ForEach:{num} 的平方是 {square},线程 ID={Task.CurrentId}");Task.Delay(150).Wait();});Console.WriteLine("Parallel.ForEach 结束");}
}
输出特点:循环/遍历的顺序是无序的(多个线程同时执行),线程 ID 可能重复(线程池复用线程)。
取消并行操作(CancellationToken
)
using System;
using System.Threading;
using System.Threading.Tasks;class ParallelCancellationDemo
{static void Main(){CancellationTokenSource cts = new CancellationTokenSource();// 启动一个任务,300ms 后取消并行操作Task.Run(() => {Task.Delay(300).Wait();cts.Cancel(); // 发送取消信号Console.WriteLine("已发送取消信号");});try{// 并行循环,支持取消Parallel.For(0, 100, new ParallelOptions{CancellationToken = cts.Token, // 传入取消令牌MaxDegreeOfParallelism = 4 // 最大并行度(同时运行的线程数)}, i => {Console.WriteLine($"处理 i={i}");Task.Delay(100).Wait(); // 模拟计算cts.Token.ThrowIfCancellationRequested(); // 检查是否取消});}catch (OperationCanceledException){Console.WriteLine("并行操作已取消");}}
}
优缺点
- 优点:自动利用多核 CPU,简化并行循环代码,适合 CPU 密集型任务。
- 缺点:
- 不适合依赖顺序的任务(并行执行顺序无序);
- 过度并行(如
MaxDegreeOfParallelism
过大)会导致线程切换开销增加,反而降低性能。
5. PLINQ(Parallel LINQ)
PLINQ 是 LINQ 的并行版本,通过 AsParallel()
将查询转换为并行执行,自动利用多核处理大数据集。
基本用法
using System;
using System.Linq;class PlinqDemo
{static void Main(){// 生成 10000 个随机数int[] numbers = Enumerable.Range(1, 10000).Select(_ => new Random().Next(1, 1000)).ToArray();// 普通 LINQ(单线程):筛选偶数并计算平方和var normalQuery = numbers.Where(n => n % 2 == 0).Select(n => n * n).Sum();Console.WriteLine($"普通 LINQ 结果:{normalQuery}");// PLINQ(并行):通过 AsParallel() 启用并行var parallelQuery = numbers.AsParallel() // 转换为并行查询.Where(n => n % 2 == 0).Select(n => n * n).Sum();Console.WriteLine($"PLINQ 结果:{parallelQuery}"); // 结果与普通 LINQ 相同(计算逻辑一致)}
}
关键操作
AsParallel()
:将序列转换为并行查询。WithDegreeOfParallelism(n)
:限制并行度(最多 n 个线程)。AsOrdered()
:保持查询结果的顺序(默认并行查询结果无序)。
三、线程安全:避免竞态条件
多线程共享数据时,若多个线程同时读写同一资源,可能导致竞态条件(数据不一致)。.NET Core 提供了多种线程同步机制保证线程安全。
1. lock
语句(最常用)
lock
确保同一时间只有一个线程执行代码块,本质是对 Monitor.Enter
和 Monitor.Exit
的封装。
using System;
using System.Threading.Tasks;class LockDemo
{private static int _count = 0;private static readonly object _lockObj = new object(); // 锁对象(必须是引用类型,且唯一)static void Main(){// 10 个线程同时递增 _countTask[] tasks = new Task[10];for (int i = 0; i < 10; i++){tasks[i] = Task.Run(IncrementCount);}Task.WaitAll(tasks);Console.WriteLine($"最终 count 值:{_count}"); // 若不加锁,结果可能小于 10000;加锁后一定是 10000}static void IncrementCount(){for (int i = 0; i < 1000; i++){lock (_lockObj) // 同一时间只有一个线程进入此代码块{_count++; // 临界区:对共享资源的操作}}}
}
注意:
- 锁对象必须是
static readonly
(避免被修改或回收); - 锁的范围应尽可能小(只包裹临界区),减少线程阻塞时间。
2. Interlocked
类:原子操作
Interlocked
提供原子操作(不可中断的操作),适合简单的数值增减、赋值等,性能优于 lock
。
using System;
using System.Threading;
using System.Threading.Tasks;class InterlockedDemo
{private static int _count = 0;static void Main(){Task[] tasks = new Task[10];for (int i = 0; i < 10; i++){tasks[i] = Task.Run(IncrementCount);}Task.WaitAll(tasks);Console.WriteLine($"最终 count 值:{_count}"); // 10000}static void IncrementCount(){for (int i = 0; i < 1000; i++){Interlocked.Increment(ref _count); // 原子递增(线程安全)}}
}
常用方法:Increment
(递增)、Decrement
(递减)、Add
(相加)、Exchange
(赋值)、CompareExchange
(比较并交换)。
3. 并发集合(System.Collections.Concurrent
)
为多线程设计的集合类,无需手动加锁即可安全访问,如:
ConcurrentDictionary<TKey, TValue>
:线程安全的字典;ConcurrentQueue<T>
:线程安全的队列;ConcurrentStack<T>
:线程安全的栈。
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;class ConcurrentCollectionDemo
{static void Main(){// 线程安全的字典ConcurrentDictionary<int, string> dict = new ConcurrentDictionary<int, string>();// 10 个线程同时添加键值对Parallel.For(0, 10, i => {// TryAdd:原子操作,添加成功返回 truebool added = dict.TryAdd(i, $"Value_{i}");Console.WriteLine($"添加 {i}: {added}");});// 遍历结果foreach (var item in dict){Console.WriteLine($"{item.Key}: {item.Value}");}}
}
四、多线程适用场景与最佳实践
1. 适用场景
- CPU 密集型任务:如数据分析、数学计算、图像处理等,适合用
Parallel
、PLINQ
或Task.Run
(利用多核并行计算)。 - I/O 密集型任务:如文件读写、数据库操作、网络请求等,适合用
async/await
(不阻塞线程,提高利用率)。
2. 最佳实践
- 优先使用
Task
/async/await
:现代 .NET 开发的推荐方式,兼顾性能和代码可读性。 - 避免创建大量线程:优先使用线程池或
Task
,减少线程创建开销。 - 最小化锁范围:
lock
只包裹必要的临界区,避免线程长时间阻塞。 - 使用并发集合:多线程操作集合时,优先用
ConcurrentDictionary
等,避免手动加锁。 - 避免
Thread.Sleep
:改用Task.Delay
(非阻塞,释放线程)。 - 正确处理异常:
Task
异常需通过await
或Task.Wait()
捕获,否则可能导致程序崩溃。
总结
.NET Core 多线程编程提供了从底层 Thread
到高层 Task
/async/await
的完整 API 体系:
- 简单短期任务:用
ThreadPool
或Task.Run
; - I/O 密集型任务:用
async/await
最大化线程利用率; - CPU 密集型并行计算:用
Parallel
或PLINQ
; - 细粒度线程控制:用
Thread
类(谨慎使用)。
核心是根据任务类型选择合适的 API,并通过 lock
、Interlocked
或并发集合保证线程安全,最终实现高效、稳定的并发程序。