一、多线程同步的必要性
当我们需要开发一个电商秒杀系统时,库存修改操作在同一毫秒可能被数百个线程访问。笔者曾经亲眼见到新手程序员在没有同步控制的情况下,商品库存出现负数这种经典竞态条件问题。正如交通信号灯维持道路秩序,线程同步机制就是多线程世界的"红绿灯"系统
二、Monitor基础与应用
2.1 核心用法
// C# .NET 6 控制台应用
object syncRoot = new object();
int sharedCounter = 0;
void IncrementCounter()
{
lock(syncRoot) // 等同于Monitor.Enter的语法糖
{
Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 进入临界区");
sharedCounter++;
Thread.Sleep(50); // 模拟业务耗时
} // 自动调用Monitor.Exit
}
// 创建三个竞争线程
Parallel.Invoke(IncrementCounter, IncrementCounter, IncrementCounter);
Console.WriteLine($"最终结果:{sharedCounter}");
/* 输出示例:
线程 4 进入临界区
线程 6 进入临界区
线程 5 进入临界区
最终结果:3 */
2.2 高级特性
在物流调度系统中,我们曾用Monitor实现过生产消费者模式:
Queue<Order> orderQueue = new Queue<Order>();
object queueLock = new object();
void Producer()
{
while(true)
{
lock(queueLock)
{
while(orderQueue.Count >= 10) // 避免队列溢出
{
Console.WriteLine("仓库已满,等待消费...");
Monitor.Wait(queueLock); // 主动释放锁并等待
}
orderQueue.Enqueue(new Order());
Console.WriteLine($"生产订单,当前库存:{orderQueue.Count}");
Monitor.Pulse(queueLock); // 通知消费者
}
Thread.Sleep(100);
}
}
void Consumer()
{
while(true)
{
lock(queueLock)
{
while(orderQueue.Count == 0)
{
Console.WriteLine("仓库缺货,等待生产...");
Monitor.Wait(queueLock);
}
orderQueue.Dequeue();
Console.WriteLine($"消费订单,剩余库存:{orderQueue.Count}");
Monitor.Pulse(queueLock);
}
Thread.Sleep(150);
}
}
2.3 应用场景
最适合保护小型临界区,比如配置数据的原子更新、单据流水号生成等。在电商平台的优惠券发放模块中,我们使用Monitor确保了库存扣减的原子性
三、Semaphore信号量机制
3.1 典型实现
数据库连接池是Semaphore的经典用例:
SemaphoreSlim pool = new SemaphoreSlim(5, 5); // 初始和最大信号量均为5
async Task AccessDatabase()
{
await pool.WaitAsync(); // 异步等待可用信号量
try
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss} 线程{Thread.CurrentThread.ManagedThreadId} 获取连接");
await Task.Delay(1000); // 模拟数据库操作
}
finally
{
pool.Release();
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} 释放连接");
}
}
// 模拟10个并发请求
var tasks = Enumerable.Range(1,10)
.Select(_ => AccessDatabase());
await Task.WhenAll(tasks);
3.2 高级形态
在文件处理系统中,我们实现过动态扩容的信号量:
SemaphoreSlim dynamicSemaphore = new SemaphoreSlim(3, 10);
void AdjustCapacity()
{
// 根据系统负载动态调整
int newCount = GetOptimalThreadCount();
dynamicSemaphore.Release(newCount - dynamicSemaphore.CurrentCount);
}
3.3 适用场景
非常适合资源池管理,如API调用限流、硬件设备访问控制。我们的IoT网关服务使用Semaphore确保同时连接的设备不超过硬件承载上限
四、ReaderWriterLockSlim读写锁
4.1 基础示例
在配置中心的热更新场景:
ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
Dictionary<string, string> configCache = new Dictionary<string, string>();
string GetConfig(string key)
{
cacheLock.EnterReadLock();
try
{
if(configCache.TryGetValue(key, out var value))
return value;
// 双检锁模式
cacheLock.ExitReadLock();
cacheLock.EnterWriteLock();
try
{
if(!configCache.ContainsKey(key))
{
// 模拟加载耗时
value = LoadFromDatabase(key);
configCache.Add(key, value);
}
return configCache[key];
}
finally
{
cacheLock.ExitWriteLock();
}
}
finally
{
if(cacheLock.IsReadLockHeld)
cacheLock.ExitReadLock();
}
}
4.2 死锁预防
在金融交易系统中曾遇到递归调用问题:
ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
void ProcessTransaction()
{
rwLock.EnterReadLock();
try
{
VerifyAccount(); // 内部可能需要升级锁
UpdateBalance();
}
finally
{
rwLock.ExitReadLock();
}
}
void VerifyAccount()
{
// 尝试升级为写锁会触发死锁
rwLock.EnterUpgradeableReadLock();
try
{
if(NeedUpdate())
{
rwLock.EnterWriteLock();
// 执行更新...
}
}
finally
{
if(rwLock.IsWriteLockHeld)
rwLock.ExitWriteLock();
rwLock.ExitUpgradeableReadLock();
}
}
4.3 最佳实践
适用于读多写少的场景,如实时行情推送系统。证券系统的委托簿更新采用ReaderWriterLockSlim,读性能比普通锁提升40%
五、三剑客横向对比
5.1 性能指标
在某压力测试中(100线程,读写比9:1):
- Monitor:平均响应时间23ms
- ReaderWriterLockSlim:17ms
- Semaphore(5并发):89ms但无资源争用
5.2 易用性分析
Monitor的语法糖最简洁,但忘记释放风险最高。我们的代码审计显示,ReaderWriterLockSlim的正确使用率仅为65%,主要问题在递归锁管理
六、选型决策树
根据项目特征选择:
- 临界区快速操作 → Monitor
- 资源池/限流 → Semaphore
- 读写差异明显 → ReaderWriterLockSlim
- 需要等待通知 → Monitor+Wait/Pulse
- 跨进程同步 → Mutex(虽未讨论但值得注意)
七、实战经验总结
7.1 调试技巧
- 使用Thread.CurrentThread.ManagedThreadId跟踪线程路径
- 在Visual Studio的并行堆栈视图中观察锁状态
- 避免在锁内调用外部服务(曾经因此导致整个系统卡死)
7.2 最佳实践
- 为每个共享资源创建独立的锁对象
- 用try-finally确保锁释放
- 限制锁的作用域至最小范围
- 监控锁的等待时间(我们的APM系统设置150ms报警阈值)
评论