欢迎光临
我们一直在努力

C#线程池和信号灯

线程池(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
主线程结束!

扩展阅读:各种多线程同步原语

多线程同步原语对比分析

特性

信号灯
(Semaphore)​


(Lock/Monitor)​

事件
(Event)​

互斥量
(Mutex)​

读写锁
(ReaderWriterLock)​

核心功能​

控制并发访问数量

互斥访问临界区

线程间事件通知

跨进程互斥访问

读共享、写独占

设计目的​

限制同时执行的线程数

确保线程安全的数据访问

线程间通信与协调

系统级资源保护

优化读多写少场景

允许并发数​

N个(可配置)

1个(严格互斥)

N/A(信号机制)

1个(跨进程互斥)

读:多个,写:1个​

线程所有权​

❌ 无所有权

✅ 有所有权
(必须同一线程释放)

❌ 无所有权

✅ 有所有权
(必须同一线程释放)

✅ 有所有权
(复杂的所有权规则)

跨进程支持​

✅ 支持
(命名信号灯)

❌ 不支持

✅ 支持
(命名事件)

✅ 支持
(命名互斥量)

❌ 不支持

性能开销​

中等(用户态为主)

低(用户态实现)

低-中等

高(涉及内核切换)

中等(读写分离)

使用复杂度​

中等

简单​

简单

中等

较高​

信号机制​

获取/释放许可
(Wait/Release)

进入/退出临界区
(Enter/Exit)

设置/等待事件
(Set/WaitOne)

获取/释放互斥量
(WaitOne/ReleaseMutex)

读锁/写锁
(EnterReadLock/EnterWriteLock)

异步支持​

✅ SemaphoreSlim.WaitAsync

❌ 无内置异步支持

✅ .NET Core+ WaitOneAsync

❌ 无内置异步支持

✅ ReaderWriterLockSlim支持

公平性​

可配置(FIFO队列)

不保证(竞争获取)

不保证(随机唤醒)

不保证(竞争获取)

可能饿死写线程

死锁风险​

中等(需正确配对Wait/Release)

高(嵌套锁易死锁)

低(简单信号)

高(跨进程更复杂)

高(升级锁易死锁)

轻量级版本​

SemaphoreSlim

Monitor(C# lock关键字)

ManualResetEventSlim

❌ 无轻量级版本

ReaderWriterLockSlim

典型应用场景​

1. 资源池限制
2. API限流
3. 生产者-消费者

1. 保护共享数据
2. 简单互斥访问
3. 方法同步

1. 线程启动/停止信号
2. 等待外部事件
3. 线程间通知

1. 单实例应用
2. 跨进程资源保护
3. 系统级互斥

1. 缓存实现
2. 配置管理
3. 读多写少的数据结构

代码示例​

semaphore.Wait()
semaphore.Release()

lock(obj) { … }

event.Set()
event.WaitOne()

mutex.WaitOne()
mutex.ReleaseMutex()

rwLock.EnterReadLock()
rwLock.ExitReadLock()

内存占用​

较小(Slim版本)

最小

较小(Slim版本)

最大(内核对象)

中等

可重入性​

❌ 不可重入
(同一线程需释放多次)

✅ 可重入
(同一线程可多次进入)

❌ 不可重入

⚠️ 部分可重入
(同一线程可重入)

⚠️ 部分可重入
(支持锁升级)

超时支持​

✅ 支持
Wait(毫秒)

✅ 支持
Monitor.TryEnter

✅ 支持
WaitOne(毫秒)

✅ 支持
WaitOne(毫秒)

✅ 支持
TryEnterReadLock

平台兼容性​

✅ 全平台支持

✅ 全平台支持

✅ 全平台支持

✅ 全平台支持
(命名互斥量跨平台)

✅ 全平台支持

推荐使用​

并发控制首选​

简单互斥首选​

简单通知场景​

跨进程同步时​

读多写少时​

.NET Core优化​

✅ SemaphoreSlim性能好

✅ lock关键字优化好

✅ ManualResetEventSlim

❌ Mutex较重

✅ ReaderWriterLockSlim

适用模式​

1. 令牌桶算法
2. 连接池
3. 限流器

1. 单例模式
2. 线程安全集合
3. 原子操作保护

1. 生产者-消费者
2. 工作线程同步
3. 状态机等待

1. 应用单实例
2. 文件锁替代
3. 系统资源保护

1. 缓存同步
2. 配置热更新
3. 观察者模式

性能排序对比(从最快到最慢)

排名

同步原语

相对速度

适用场景

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)。

    赞(0)
    未经允许不得转载:171主机测评 » C#线程池和信号灯
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址