线程池(ThreadPool)
通过 ThreadPool.QueueUserWorkItem将任务(testFun方法)排入线程池队列。
线程池会自动管理一组后台线程来执行这些任务,无需手动创建和管理线程。
加入队列后会自动执行。
调用 ThreadPool.QueueUserWorkItem后,任务会被添加到线程池的全局队列中。
线程池中的空闲线程(或新创建的线程,但受 SetMaxThreads限制)会自动从队列中取出任务并执行。
整个过程由 .NET 运行时管理,无需显式启动线程。
线程池任务的执行顺序是不确定的,主要原因包括:
a) 线程池调度机制
线程池维护一个工作队列,但多个线程可能同时从队列中取任务。由于线程调度的不确定性(如操作系统调度、线程竞争等),任务执行的开始顺序可能与加入队列的顺序不一致。
虽然任务按 FIFO(先进先出)原则加入队列,但线程池可能有多个队列(如全局队列和本地队列),进一步影响顺序。
.NET 线程池设计多个队列的核心目的是:最大化性能,最小化锁竞争,最大化吞吐量和效率。
b) 并发线程数限制
代码中设置线程池最多同时使用 5 个线程。
循环加入 10 个任务时,前 5 个任务可能立即被分配给 5 个线程执行,而剩余 5 个任务在队列中等待。
当某个线程提前完成任务(尽管这里有 Thread.Sleep(5000),但实际场景中任务耗时可能不同)时,它会从队列中取下一个任务,但取任务的顺序可能受线程竞争影响。
c) 任务执行时间的随机性
即使每个任务都有 Thread.Sleep(5000),但线程启动的微小延迟、系统负载等因素可能导致输出时间戳的顺序与任务编号顺序不一致。
例如,任务 1 的线程可能稍晚启动,而任务 2 的线程先启动,导致输出顺序为 2、1 等。
前 5 个任务(编号 1~5)可能几乎同时开始执行,输出时间接近,但顺序可能乱序。
后 5 个任务(编号 6~10)在前一批任务完成后(约 5 秒后)开始执行,同样可能出现乱序。
如果想控制执行顺序:
线程池设计用于高效执行独立、短期的任务,不保证顺序。如需顺序执行,应使用同步机制(如锁、信号量)或串行执行。
若需等待所有任务完成并按顺序处理结果,可使用 Task和相关 API(如 Task.WhenAll)配合集合排序。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace 多线程线程池
{
class Program
{
const int cycleNum = 10;
static void Main(string[] args)
{
Console.WriteLine("主线程执行!");
ThreadPool.SetMinThreads(1, 1);//设置最小线程并发数
ThreadPool.SetMaxThreads(5, 5);//设置最大线程并发数
for (int i = 1; i <= cycleNum; i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(testFun), i.ToString());//将方法排入队列以便执行,它表示要执行的方法,包含方法所用数据的对象。
}
Console.WriteLine("主线程结束!");
Console.ReadKey();
}
public static void testFun(object obj)
{
Console.WriteLine(string.Format("{0}:第{1}个线程", DateTime.Now.ToString(), obj.ToString()));
Thread.Sleep(5000);
}
}
}
主线程执行!
主线程结束!
2026/2/21 21:50:38:第1个线程
2026/2/21 21:50:38:第2个线程
2026/2/21 21:50:38:第3个线程
2026/2/21 21:50:38:第4个线程
2026/2/21 21:50:38:第5个线程
2026/2/21 21:50:43:第6个线程
2026/2/21 21:50:43:第8个线程
2026/2/21 21:50:43:第7个线程
2026/2/21 21:50:43:第9个线程
2026/2/21 21:50:44:第10个线程
信号同步检测任务完成
使用 AutoResetEvent来实现信号同步,以检测线程池任务全部完成。
同步机制:
AutoResetEvent是一个事件同步原语,它本质是一个事件对象,它的作用是进行信号同步,一个自动重置的、二元(0/1)的信号灯。
初始状态为 false(无信号状态)
当调用 Set()时变为有信号状态,自动唤醒一个等待线程,然后自动重置为无信号状态
cnt被多个线程同时修改,如果没有使用原子操作,可能产生竞争条件,导致cnt的值可能不正确,进而影响事件触发的条件。
需要使用Interlocked.Decrement来原子地减少cnt的值,以确保正确计数。
使用 Interlocked.Decrement 的原因:
1. 避免竞态条件:两个线程同时读取cnt=5,都减1后写入4(实际应该为3)
2. 确保最终 cnt == 0
Interlocked 能进行原子操作是因为:
1. 硬件支持:CPU提供原子指令(如LOCK前缀、CAS指令)
2. 内存屏障:确保内存操作的顺序性和可见性
3. 编译器/运行时支持:将高级语言调用转换为底层原子指令
4. 单一内存位置:专注于单个变量的原子操作
Interlocked的优势:
1. 用户态操作,无需进入内核态
2. 硬件原子指令,效率极高
3. 无锁设计,避免线程阻塞
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace 信号同步
{
class Program
{
const int cycleNum = 10;//线程数量
static int cnt = 10;//
static AutoResetEvent myEvent = new AutoResetEvent(false);//通知正在等待的线程已发生事件
static void Main(string[] args)
{
Console.WriteLine("主线程执行!");
ThreadPool.SetMinThreads(1, 1);//设置最小线程并发数
ThreadPool.SetMaxThreads(5, 5);//设置最大线程并发数
for (int i = 1; i <= cycleNum; i++)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(testFun), i.ToString());
}
Console.WriteLine("主线程结束!");
//创建无参的线程
Thread thread1 = new Thread(new ThreadStart(Thread1));
//调用Start方法执行线程
thread1.Start();
}
public static void testFun(object obj)
{
Console.WriteLine(string.Format("{0}:第{1}个线程", DateTime.Now.ToString(), obj.ToString()));
Thread.Sleep(1000);
// 使用Interlocked进行原子操作
int remaining = Interlocked.Decrement(ref cnt);
Console.WriteLine($"{DateTime.Now}:第{obj}个线程完成,剩余{remaining}个任务");
if (remaining == 0)
{
myEvent.Set();
}
}
/// <summary>
/// 创建线程池任务结束无参的方法
/// </summary>
static void Thread1()
{
myEvent.WaitOne();//等待set信号
Console.WriteLine("线程池终止!");
Console.ReadKey();
}
}
}
主线程执行!
主线程结束!
2026/2/21 21:50:02:第2个线程
2026/2/21 21:50:02:第4个线程
2026/2/21 21:50:02:第3个线程
2026/2/21 21:50:02:第1个线程
2026/2/21 21:50:02:第5个线程
2026/2/21 21:50:03:第2个线程完成,剩余9个任务
2026/2/21 21:50:04:第5个线程完成,剩余8个任务
2026/2/21 21:50:04:第6个线程
2026/2/21 21:50:04:第3个线程完成,剩余6个任务
2026/2/21 21:50:04:第7个线程
2026/2/21 21:50:04:第1个线程完成,剩余7个任务
2026/2/21 21:50:04:第8个线程
2026/2/21 21:50:04:第4个线程完成,剩余5个任务
2026/2/21 21:50:04:第9个线程
2026/2/21 21:50:04:第10个线程
2026/2/21 21:50:05:第8个线程完成,剩余3个任务
2026/2/21 21:50:05:第7个线程完成,剩余1个任务
2026/2/21 21:50:05:第6个线程完成,剩余4个任务
2026/2/21 21:50:05:第9个线程完成,剩余2个任务
2026/2/21 21:50:05:第10个线程完成,剩余0个任务
线程池终止!
信号灯限制并发
使用 信号灯(SemaphoreSlim) 机制实现了并发数限制和任务完成等待两个关键功能。
任务执行流程:
┌───────—————───────────┐
│ 1. 等待semaphore许可(控制并发) │
│ ↓ │
│ 2. 执行任务(最多3个并发) │
│ ↓ │
│ 3. 释放semaphore许可(允许新任务进入) │
│ ↓ │
│ 4. 释放completionSemaphore(通知完成) │
└───────────────────────┘
semaphore- 并发控制器
作用:限制同时执行的任务数量(3个)
工作原理:
初始有3个"许可"(initialCount=3)
每个任务开始前调用 semaphore.Wait()获取许可
如果没有可用许可(已有3个任务在执行),任务会阻塞等待
任务完成后调用 semaphore.Release()归还许可
即使线程池有10个线程可用,但最多只有3个任务能同时执行
这是因为 semaphore 控制的是逻辑并发,不是物理线程数
时间线示例:
时刻1: 任务1、2、3 获得许可,开始执行
时刻2: 任务1完成,释放许可,任务4获得许可,开始执行
时刻3: 任务2完成,释放许可,任务5获得许可,开始执行
… 以此类推
等待线程流程:
┌──────────────────────┐
│ 循环10次,每次等待completionSemaphore │
│ 每收到一个信号 = 一个任务完成 │
│ 收到10个信号后 = 所有任务完成 │
└──────────────────────┘
completionSemaphore- 完成计数器
作用:跟踪和等待所有任务完成
工作原理:
初始0个许可(initialCount=0),等待线程会阻塞
每个任务完成后调用 completionSemaphore.Release()增加一个许可
等待线程循环10次调用 completionSemaphore.Wait()等待所有许可
completionSemaphore 的初始状态:
可用许可 = 0
最大许可 = 10
当所有任务完成后:
每个任务调用 completionSemaphore.Release() → 可用许可增加1
10个任务完成后 → 可用许可 = 10
等待线程:
循环10次调用 completionSemaphore.Wait()
第一次调用时,可用许可=0 → 阻塞
当第一个任务完成 → 可用许可=1 → 等待线程唤醒,消费一个许可
循环10次后 → 所有任务完成
using System;
using System.Threading;
namespace 信号灯
{
class Program
{
const int cycleNum = 10; // 总任务数
static int cnt = 10; // 剩余任务计数器
static SemaphoreSlim semaphore = new SemaphoreSlim(3, 3); // 控制并发数:最多3个同时执行
static SemaphoreSlim completionSemaphore = new SemaphoreSlim(0, cycleNum); // 任务完成信号灯
static void Main(string[] args)
{
Console.WriteLine("主线程开始!");
ThreadPool.SetMinThreads(1, 1);
ThreadPool.SetMaxThreads(10, 10);
// 启动所有任务
for (int i = 1; i <= cycleNum; i++)
{
ThreadPool.QueueUserWorkItem(testFun, i.ToString());
}
Console.WriteLine("所有任务已提交到线程池!");
// 创建等待线程
Thread waitThread = new Thread(new ThreadStart(WaitForCompletion));
waitThread.Start();
Console.WriteLine("主线程继续执行其他工作…");
Thread.Sleep(500); // 模拟主线程做其他事情
waitThread.Join(); // 等待等待线程结束
Console.WriteLine("主线程结束!");
Console.ReadKey();
}
public static void testFun(object obj)
{
// 等待获取执行许可(控制并发数)
semaphore.Wait();
try
{
string taskId = obj.ToString();
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}:任务{taskId} 开始执行(并发控制)");
// 模拟工作
Thread.Sleep(1000 + new Random().Next(500)); // 随机延时1-1.5秒
// 原子递减计数器
int remaining = Interlocked.Decrement(ref cnt);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}:任务{taskId} 完成,剩余任务: {remaining}");
}
finally
{
// 释放执行许可
semaphore.Release();
// 释放一个完成信号
completionSemaphore.Release();
}
}
/// <summary>
/// 等待所有任务完成的线程
/// </summary>
static void WaitForCompletion()
{
Console.WriteLine("等待线程:开始等待所有任务完成…");
// 等待所有任务完成(获取所有完成信号)
for (int i = 0; i < cycleNum; i++)
{
completionSemaphore.Wait();
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}:收到第{i + 1}个任务完成信号");
}
Console.WriteLine("等待线程:所有任务已完成!");
Console.WriteLine($"最终剩余任务数: {cnt}");
}
}
}
主线程开始!
所有任务已提交到线程池!
主线程继续执行其他工作…
等待线程:开始等待所有任务完成…
21:49:05.980:任务1 开始执行(并发控制)
21:49:05.980:任务2 开始执行(并发控制)
21:49:05.980:任务4 开始执行(并发控制)
21:49:07.385:任务4 完成,剩余任务: 9
21:49:07.386:收到第1个任务完成信号
21:49:07.386:任务3 开始执行(并发控制)
21:49:07.479:任务1 完成,剩余任务: 8
21:49:07.479:收到第2个任务完成信号
21:49:07.479:任务5 开始执行(并发控制)
21:49:07.485:任务2 完成,剩余任务: 7
21:49:07.485:收到第3个任务完成信号
21:49:07.485:任务6 开始执行(并发控制)
21:49:08.484:任务3 完成,剩余任务: 6
21:49:08.484:收到第4个任务完成信号
21:49:08.484:任务7 开始执行(并发控制)
21:49:08.531:任务6 完成,剩余任务: 5
21:49:08.531:收到第5个任务完成信号
21:49:08.531:任务8 开始执行(并发控制)
21:49:08.812:任务5 完成,剩余任务: 4
21:49:08.812:收到第6个任务完成信号
21:49:08.812:任务9 开始执行(并发控制)
21:49:09.589:任务8 完成,剩余任务: 3
21:49:09.589:收到第7个任务完成信号
21:49:09.589:任务10 开始执行(并发控制)
21:49:09.779:任务7 完成,剩余任务: 2
21:49:09.780:收到第8个任务完成信号
21:49:10.184:任务9 完成,剩余任务: 1
21:49:10.184:收到第9个任务完成信号
21:49:10.740:任务10 完成,剩余任务: 0
21:49:10.740:收到第10个任务完成信号
等待线程:所有任务已完成!
最终剩余任务数: 0
主线程结束!
扩展阅读:各种多线程同步原语
多线程同步原语对比分析
|
核心功能 |
控制并发访问数量 |
互斥访问临界区 |
线程间事件通知 |
跨进程互斥访问 |
读共享、写独占 |
|
设计目的 |
限制同时执行的线程数 |
确保线程安全的数据访问 |
线程间通信与协调 |
系统级资源保护 |
优化读多写少场景 |
|
允许并发数 |
N个(可配置) |
1个(严格互斥) |
N/A(信号机制) |
1个(跨进程互斥) |
读:多个,写:1个 |
|
线程所有权 |
❌ 无所有权 |
✅ 有所有权 |
❌ 无所有权 |
✅ 有所有权 |
✅ 有所有权 |
|
跨进程支持 |
✅ 支持 |
❌ 不支持 |
✅ 支持 |
✅ 支持 |
❌ 不支持 |
|
性能开销 |
中等(用户态为主) |
低(用户态实现) |
低-中等 |
高(涉及内核切换) |
中等(读写分离) |
|
使用复杂度 |
中等 |
简单 |
简单 |
中等 |
较高 |
|
信号机制 |
获取/释放许可 |
进入/退出临界区 |
设置/等待事件 |
获取/释放互斥量 |
读锁/写锁 |
|
异步支持 |
✅ SemaphoreSlim.WaitAsync |
❌ 无内置异步支持 |
✅ .NET Core+ WaitOneAsync |
❌ 无内置异步支持 |
✅ ReaderWriterLockSlim支持 |
|
公平性 |
可配置(FIFO队列) |
不保证(竞争获取) |
不保证(随机唤醒) |
不保证(竞争获取) |
可能饿死写线程 |
|
死锁风险 |
中等(需正确配对Wait/Release) |
高(嵌套锁易死锁) |
低(简单信号) |
高(跨进程更复杂) |
高(升级锁易死锁) |
|
轻量级版本 |
SemaphoreSlim |
Monitor(C# lock关键字) |
ManualResetEventSlim |
❌ 无轻量级版本 |
ReaderWriterLockSlim |
|
典型应用场景 |
1. 资源池限制 |
1. 保护共享数据 |
1. 线程启动/停止信号 |
1. 单实例应用 |
1. 缓存实现 |
|
代码示例 |
semaphore.Wait() |
lock(obj) { … } |
event.Set() |
mutex.WaitOne() |
rwLock.EnterReadLock() |
|
内存占用 |
较小(Slim版本) |
最小 |
较小(Slim版本) |
最大(内核对象) |
中等 |
|
可重入性 |
❌ 不可重入 |
✅ 可重入 |
❌ 不可重入 |
⚠️ 部分可重入 |
⚠️ 部分可重入 |
|
超时支持 |
✅ 支持 |
✅ 支持 |
✅ 支持 |
✅ 支持 |
✅ 支持 |
|
平台兼容性 |
✅ 全平台支持 |
✅ 全平台支持 |
✅ 全平台支持 |
✅ 全平台支持 |
✅ 全平台支持 |
|
推荐使用 |
并发控制首选 |
简单互斥首选 |
简单通知场景 |
跨进程同步时 |
读多写少时 |
|
.NET Core优化 |
✅ SemaphoreSlim性能好 |
✅ lock关键字优化好 |
✅ ManualResetEventSlim |
❌ Mutex较重 |
✅ ReaderWriterLockSlim |
|
适用模式 |
1. 令牌桶算法 |
1. 单例模式 |
1. 生产者-消费者 |
1. 应用单实例 |
1. 缓存同步 |
性能排序对比(从最快到最慢)
|
1 |
锁(Lock/Monitor) |
⭐⭐⭐⭐⭐ |
简单互斥、数据保护 |
|
2 |
读写锁(ReaderWriterLockSlim) |
⭐⭐⭐⭐ |
读多写少、缓存 |
|
3 |
信号灯(SemaphoreSlim) |
⭐⭐⭐ |
并发控制、资源池 |
|
4 |
事件(ManualResetEventSlim) |
⭐⭐ |
线程通知、状态同步 |
|
5 |
事件(AutoResetEvent) |
⭐⭐ |
简单信号通知 |
|
6 |
互斥量(Mutex) |
⭐ |
跨进程同步、系统级 |
现代C#使用建议
|
信号灯 |
Semaphore |
SemaphoreSlim |
用户态优先、支持异步 |
|
事件 |
AutoResetEvent/ManualResetEvent |
ManualResetEventSlim |
性能更好、内存更少 |
|
读写锁 |
ReaderWriterLock |
ReaderWriterLockSlim |
性能更好、支持锁升级 |
|
锁 |
Monitor |
lock关键字 |
语法简洁、编译器优化 |
常见陷阱总结
|
死锁 |
Lock, Mutex, ReaderWriterLock |
按固定顺序获取锁、使用超时 |
|
忘记释放 |
所有(特别是Mutex) |
使用try-finally、using语句 |
|
竞态条件 |
Lock, Semaphore |
原子操作、双重检查 |
|
信号丢失 |
AutoResetEvent |
使用ManualResetEvent或标志位 |
|
性能瓶颈 |
Mutex, 不当的锁粒度 |
减小锁范围、使用读写锁 |
总结
-
信号灯(Semaphore):"最多允许N个线程同时进入"
-
锁(Lock):"一次只能一个线程进入"
-
事件(Event):"我准备好了,你可以开始了"
-
互斥量(Mutex):"跨进程的一次只能一个"
-
读写锁(ReaderWriterLock):"读可以共享,写必须独占"
实际项目选择
Web应用API限流 → SemaphoreSlim
缓存数据读取 → ReaderWriterLockSlim
数据库连接池 → SemaphoreSlim
单例模式实现 → lock(双检锁)
应用单实例检测 → Mutex
线程启动协调 → ManualResetEventSlim
配置热更新 → ReaderWriterLockSlim
跨进程日志写入 → Mutex
黄金法则:优先使用最简单的同步原语(lock),只在必要时才升级到更复杂的机制(Semaphore/ReaderWriterLock)。



