1. 信号量与锁的本质区别
在C#多线程编程中,信号量(Semaphore)和锁(Lock)都是用于线程同步的机制,但它们的适用场景和工作原理有本质差异。锁更像是单间厕所的门锁——同一时间只允许一个线程进入临界区,而信号量则像公共厕所的坑位计数器——允许有限数量的线程同时访问资源。
1.1 锁的核心特性
C#中最常用的lock关键字实际上是Monitor类的语法糖。当执行以下代码时:
csharp复制private static readonly object _lockObj = new object();
lock(_lockObj)
{
// 临界区代码
}
CLR会在底层执行Monitor.Enter和Monitor.Exit。关键特性包括:
- 排他性:获得锁的线程拥有对资源的独占访问权
- 递归性:同一线程可重复获取已持有的锁(锁计数递增)
- 内存屏障:隐含内存栅栏,保证变量可见性
注意:lock语句块结束时自动释放锁,即使抛出异常也会释放,这是通过finally块实现的
1.2 信号量的运作机制
Semaphore类通过计数器控制并发访问:
csharp复制// 允许3个线程同时访问
Semaphore semaphore = new Semaphore(3, 3);
void AccessResource()
{
semaphore.WaitOne();
try {
// 受保护的资源访问
}
finally {
semaphore.Release();
}
}
信号量的关键特点:
- 资源配额:初始化时指定最大并发数
- 跨进程可用:命名信号量可用于进程间同步
- 非所有者释放:任何线程都可调用Release()
2. 使用场景对比分析
2.1 锁的典型应用场景
- 共享数据修改:如对List的并发写入
csharp复制private List<int> _sharedList = new List<int>();
private readonly object _listLock = new object();
void AddItem(int item)
{
lock(_listLock) {
_sharedList.Add(item);
}
}
- 单例模式实现:双重检查锁定
csharp复制private static Singleton _instance;
private static readonly object _lock = new object();
public static Singleton Instance
{
get
{
if (_instance == null) {
lock(_lock) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance;
}
}
2.2 信号量的适用场景
- 连接池管理:控制数据库连接等有限资源
csharp复制class ConnectionPool
{
private Semaphore _semaphore;
private Queue<DbConnection> _pool = new Queue<DbConnection>();
public ConnectionPool(int maxConnections)
{
_semaphore = new Semaphore(maxConnections, maxConnections);
// 初始化连接池...
}
public DbConnection GetConnection()
{
_semaphore.WaitOne();
lock(_pool) {
return _pool.Dequeue();
}
}
public void ReleaseConnection(DbConnection conn)
{
lock(_pool) {
_pool.Enqueue(conn);
}
_semaphore.Release();
}
}
- 限流控制:如API调用频率限制
csharp复制// 限制每秒最多10次调用
SemaphoreSlim _throttler = new SemaphoreSlim(10, 10);
async Task CallApi()
{
await _throttler.WaitAsync();
try {
// 调用API...
}
finally {
_throttler.Release();
}
}
3. 底层实现原理剖析
3.1 锁的CLR实现
Monitor类在Windows平台使用CRITICAL_SECTION实现,在Unix-like系统使用pthread_mutex_t。关键过程:
- 快速路径尝试:通过原子操作直接获取锁
- 自旋等待:约30-50次CPU周期自旋(避免上下文切换)
- 内核等待:最终进入内核态等待(通过事件对象)
锁的升级过程:
code复制线程尝试获取锁 → 快速路径 → 自旋等待 → 进入等待队列
3.2 信号量的内核对象
Semaphore内部使用Windows内核信号量对象(CreateSemaphore)或Unix信号量(sem_init)。主要开销:
- 用户态到内核态的上下文切换(约1000+ CPU周期)
- 内核对象管理开销
- 线程调度延迟
性能对比测试数据(10万次操作):
| 操作类型 | 耗时(ms) |
|---|---|
| lock | 15 |
| Semaphore | 320 |
| SemaphoreSlim | 45 |
4. 高级用法与陷阱规避
4.1 锁的进阶技巧
- 避免锁粒度过大:
csharp复制// 错误示范 - 锁住整个方法
lock(_lock) {
ReadConfig();
ProcessData();
SaveResult();
}
// 正确做法 - 细粒度锁定
var temp = ReadConfig();
lock(_lock) {
ProcessData(temp);
}
lock(_lock) {
SaveResult();
}
- 死锁预防:按固定顺序获取多个锁
csharp复制// 定义全局锁获取顺序
private static readonly object _lock1 = new object();
private static readonly object _lock2 = new object();
void MethodA()
{
lock(_lock1) {
lock(_lock2) {
// ...
}
}
}
void MethodB()
{
// 保持相同获取顺序
lock(_lock1) {
lock(_lock2) {
// ...
}
}
}
4.2 信号量使用陷阱
- 信号量泄露(忘记Release):
csharp复制// 错误示范 - 可能因异常导致信号量泄露
semaphore.WaitOne();
DoWork(); // 可能抛出异常
semaphore.Release();
// 正确做法 - 使用using或try-finally
semaphore.WaitOne();
try {
DoWork();
}
finally {
semaphore.Release();
}
- 信号量初始值设置不当:
csharp复制// 错误示范 - 最大并发数大于实际资源数
Semaphore semaphore = new Semaphore(5, 5); // 但只有3个资源
// 正确做法 - 匹配实际资源数量
Semaphore semaphore = new Semaphore(3, 3);
5. 性能优化实战
5.1 轻量级替代方案
- 对于简单场景,考虑Interlocked类:
csharp复制// 原子计数器替代锁
int _counter = 0;
void Increment()
{
Interlocked.Increment(ref _counter);
}
- ReaderWriterLockSlim适用于读多写少场景:
csharp复制private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
void ReadData()
{
_rwLock.EnterReadLock();
try {
// 读取操作...
}
finally {
_rwLock.ExitReadLock();
}
}
void WriteData()
{
_rwLock.EnterWriteLock();
try {
// 写入操作...
}
finally {
_rwLock.ExitWriteLock();
}
}
5.2 混合模式同步
结合锁和信号量的优势:
csharp复制class HybridSync
{
private readonly SemaphoreSlim _semaphore;
private readonly object _lock = new object();
private int _activeCount = 0;
public HybridSync(int maxConcurrency)
{
_semaphore = new SemaphoreSlim(maxConcurrency);
}
public async Task<T> ExecuteAsync<T>(Func<Task<T>> taskFactory)
{
await _semaphore.WaitAsync();
try {
lock(_lock) {
_activeCount++;
Console.WriteLine($"Active: {_activeCount}");
}
return await taskFactory();
}
finally {
lock(_lock) {
_activeCount--;
}
_semaphore.Release();
}
}
}
6. 分布式环境下的选择
6.1 分布式锁实现
在微服务架构中,本地锁不再适用,常见方案:
- Redis RedLock算法
csharp复制// 使用StackExchange.Redis实现
var redis = ConnectionMultiplexer.Connect("localhost");
var redlockFactory = RedLockFactory.Create(redis);
using (var redLock = await redlockFactory.CreateLockAsync(
"resource-name",
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(1)))
{
if (redLock.IsAcquired) {
// 获取锁成功...
}
}
- ZooKeeper临时顺序节点
- 数据库乐观锁(版本号控制)
6.2 分布式信号量模式
使用Redis实现分布式信号量:
csharp复制var redis = ConnectionMultiplexer.Connect("localhost");
var db = redis.GetDatabase();
// 获取信号量
bool acquired = db.StringSet("semaphore:resource", "1",
TimeSpan.FromSeconds(30),
when: When.NotExists);
if (acquired) {
try {
// 执行业务逻辑...
}
finally {
db.KeyDelete("semaphore:resource");
}
}
在实际项目中,我通常会根据以下决策树选择同步机制:
- 是否需要跨进程同步? → 是:考虑命名信号量/Mutex
- 是否需要控制并发数量? → 是:使用信号量
- 是否简单互斥访问? → 是:优先选择lock
- 读多写少场景? → 是:考虑ReaderWriterLockSlim
- 分布式环境? → 是:采用Redis/ZooKeeper方案
