悠悠楠杉
C线程同步实战:让多线程编程更优雅高效
一、线程同步的必要性
当多个线程同时访问共享资源时,就会出现经典的"竞态条件"问题。我曾在一个电商库存管理系统里,亲眼目睹因为未做同步导致的超卖事故——两个线程同时读取库存余量时都显示有货,结果总计卖出数量却超过了实际库存。
csharp
// 典型的不安全代码示例
private int _stock = 100;
void ReduceStock()
{
if(_stock > 0){
Thread.Sleep(10); // 模拟处理延迟
_stock--;
}
}
这种场景下,线程同步就不是"最好有"而是"必须有"的解决方案。C#提供了从轻量级到重量级的多种同步机制,我们需要根据具体场景作出合适选择。
二、核心同步方案详解
1. lock关键字(最常用方案)
csharp
private readonly object _lockObj = new object();
void SafeMethod()
{
lock(_lockObj)
{
// 临界区代码
}
}
最佳实践:
- 锁定专用私有对象而非Type实例或this
- 保持锁区块代码尽量简短
- 避免在锁内调用外部方法
去年优化日志系统时,我们通过将lock对象从typeof(Logger)改为实例字段,性能提升了40%。这是因为Type对象是全局的,会导致无关代码的阻塞。
2. Monitor类(lock的底层实现)
csharp
Monitor.Enter(_lockObj);
try
{
// 临界区
}
finally
{
Monitor.Exit(_lockObj);
}
高级用法包括TryEnter()设置超时:
csharp
if(Monitor.TryEnter(_lockObj, 500))
{
try { /* 操作 */ }
finally { Monitor.Exit(_lockObj); }
}
else
{
// 处理超时逻辑
}
3. Mutex(跨进程同步)
在开发分布式任务调度器时,我们使用Mutex确保集群中只有一个节点执行定时任务:
csharp
using Mutex mutex = new Mutex(true, "Global\\MyAppTask", out bool createdNew);
if(createdNew)
{
// 获得锁执行业务
}
else
{
Console.WriteLine("其他进程正在运行");
}
注意点:
- 命名Mutex需要前缀"Global\"或"Local\"
- 比lock更重,适合跨进程场景
4. Semaphore(资源池控制)
数据库连接池的典型实现:
csharp
SemaphoreSlim _pool = new SemaphoreSlim(10, 10);
async Task UseDatabase()
{
await _pool.WaitAsync();
try {
// 使用连接
}
finally {
_pool.Release();
}
}
Semaphore特别适合控制有限资源的并发访问量。
5. 读写锁(ReaderWriterLockSlim)
在配置中心项目中,我们采用读写锁优化配置加载:
csharp
private ReaderWriterLockSlim _rwLock = new();
string GetConfig()
{
_rwLock.EnterReadLock();
try { return _config; }
finally { _rwLock.ExitReadLock(); }
}
void UpdateConfig(string val)
{
_rwLock.EnterWriteLock();
try { _config = val; }
finally { _rwLock.ExitWriteLock(); }
}
这种方案在读多写少的场景下性能显著优于lock。
三、同步方案选型指南
| 方案 | 适用场景 | 性能开销 |
|---------------------|---------------------------------|---------|
| lock | 通用短时临界区保护 | 低 |
| Monitor | 需要超时控制的同步 | 中 |
| Mutex | 跨进程资源独占 | 高 |
| Semaphore | 资源池/限流控制 | 中 |
| ReaderWriterLockSlim | 读多写少的数据访问 | 读低写高 |
经验法则:
- 90%场景下lock足够
- 涉及IO操作时考虑异步锁(SemaphoreSlim.WaitAsync)
- 避免嵌套锁,容易导致死锁
- 静态成员使用单独的静态锁对象
四、避坑指南
- 死锁预防:按照固定顺序获取多个锁
- 锁粒度:过粗影响性能,过细增加复杂度
- 不变量保护:复合操作需要整体同步
去年排查过一个生产问题:两个线程互相等待对方持有的锁导致系统挂起。最终我们引入锁超时机制和死锁检测日志才彻底解决。
总结:线程同步是构建稳健并发系统的基石。理解各方案的实现原理和适用场景,才能像交响乐指挥家那样,让多线程程序既保持活力又井然有序。记住:没有最好的同步机制,只有最合适的解决方案。