一、多线程同步的必要性
在C#开发中,多线程编程就像是一个热闹的厨房,多个厨师(线程)同时工作可以提高效率,但如果大家都争抢同一把菜刀(共享资源),就可能出现切到手(数据竞争)的情况。这时候就需要同步机制来协调工作。
想象一下,如果多个线程同时修改同一个银行账户余额,没有同步机制的话,最终结果可能会乱套。这就是为什么我们需要Monitor、Semaphore和ReaderWriterLockSlim这些"厨房管理员"来维持秩序。
二、Monitor:最基础的同步锁
Monitor就像是厨房里的一把钥匙,谁拿到钥匙谁就能使用炉灶(临界区)。它是C#中最基础的同步机制,通过lock关键字提供了简洁的语法糖。
// 技术栈:C# .NET 6
class BankAccount
{
private decimal _balance = 1000m;
private readonly object _balanceLock = new object();
public void Withdraw(decimal amount)
{
// 使用Monitor进入临界区
lock (_balanceLock)
{
if (_balance >= amount)
{
Console.WriteLine($"余额足够,正在取出{amount}元...");
_balance -= amount;
Console.WriteLine($"取款成功,剩余余额:{_balance}");
}
else
{
Console.WriteLine("余额不足,取款失败");
}
}
}
// 等价于上面的lock语法糖
public void Deposit(decimal amount)
{
Monitor.Enter(_balanceLock);
try
{
_balance += amount;
Console.WriteLine($"存款成功,当前余额:{_balance}");
}
finally
{
Monitor.Exit(_balanceLock);
}
}
}
Monitor的特点:
- 简单易用,适合保护小段代码
- 同一线程可以多次获取同一个锁(可重入)
- 不支持超时设置(除非使用Monitor.TryEnter)
- 不区分读写操作
三、Semaphore:控制并发数量的红绿灯
Semaphore就像餐厅门口的排队系统,它限制了同时进入餐厅(访问资源)的人数。与Monitor不同,Semaphore不是排他锁,它允许多个线程同时访问资源,但数量受控。
// 技术栈:C# .NET 6
class DatabaseConnectionPool
{
private readonly Semaphore _semaphore;
private readonly List<DbConnection> _connections;
public DatabaseConnectionPool(int maxConnections)
{
_semaphore = new Semaphore(maxConnections, maxConnections);
_connections = new List<DbConnection>(maxConnections);
// 初始化连接池
for (int i = 0; i < maxConnections; i++)
{
_connections.Add(new SqlConnection("连接字符串"));
}
}
public DbConnection GetConnection()
{
// 等待获取信号量(如果已满则阻塞)
_semaphore.WaitOne();
lock (_connections)
{
var conn = _connections[0];
_connections.RemoveAt(0);
return conn;
}
}
public void ReleaseConnection(DbConnection conn)
{
lock (_connections)
{
_connections.Add(conn);
}
// 释放信号量
_semaphore.Release();
}
}
Semaphore的特点:
- 控制同时访问资源的线程数量
- 可用于跨进程同步
- 有命名信号量,可用于进程间通信
- 支持超时设置
- 不区分读写操作
四、ReaderWriterLockSlim:读写分离的智能锁
ReaderWriterLockSlim就像是图书馆的管理系统,允许多个读者同时阅读(读锁),但写作时(写锁)需要独占整个图书馆。这种设计显著提高了读多写少场景的性能。
// 技术栈:C# .NET 6
class ConfigManager
{
private readonly Dictionary<string, string> _configs = new();
private readonly ReaderWriterLockSlim _lock = new();
// 读取配置(允许多个线程同时读取)
public string GetConfig(string key)
{
_lock.EnterReadLock();
try
{
if (_configs.TryGetValue(key, out var value))
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}读取配置{key}");
return value;
}
return null;
}
finally
{
_lock.ExitReadLock();
}
}
// 更新配置(独占访问)
public void UpdateConfig(string key, string value)
{
_lock.EnterWriteLock();
try
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}更新配置{key}");
_configs[key] = value;
Thread.Sleep(100); // 模拟耗时操作
}
finally
{
_lock.ExitWriteLock();
}
}
// 可升级锁(从读锁升级为写锁)
public void AddOrUpdateConfig(string key, string value)
{
_lock.EnterUpgradeableReadLock();
try
{
if (_configs.ContainsKey(key))
{
_lock.EnterWriteLock();
try
{
_configs[key] = value;
}
finally
{
_lock.ExitWriteLock();
}
}
else
{
_lock.EnterWriteLock();
try
{
_configs.Add(key, value);
}
finally
{
_lock.ExitWriteLock();
}
}
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
}
ReaderWriterLockSlim的特点:
- 区分读写操作,提高读多写少场景的性能
- 支持锁升级(从读锁升级为写锁)
- 比旧版ReaderWriterLock性能更好
- 不支持跨进程同步
- 比Monitor和Semaphore更复杂
五、三种同步机制的比较与选择指南
现在我们已经了解了这三种同步机制,让我们从几个维度进行比较:
性能:
- Monitor:中等,适合短临界区
- Semaphore:中等,取决于许可数量
- ReaderWriterLockSlim:读多写少场景下性能最佳
使用复杂度:
- Monitor:最简单
- Semaphore:中等
- ReaderWriterLockSlim:最复杂
适用场景:
- Monitor:简单的排他访问
- Semaphore:资源池、限流
- ReaderWriterLockSlim:配置管理、缓存
线程关系:
- Monitor:同一线程可重入
- Semaphore:不关心线程关系
- ReaderWriterLockSlim:支持锁升级
选择建议:
- 如果你只需要简单的互斥访问,用Monitor就够了
- 如果需要限制并发数量,选择Semaphore
- 如果是读多写少的场景,ReaderWriterLockSlim是最佳选择
- 对于跨进程同步,Semaphore是唯一选择
六、实际应用中的注意事项
在使用这些同步机制时,有几个常见的坑需要注意:
死锁风险:
- 避免嵌套获取多个锁
- 按固定顺序获取多个锁
- 使用Monitor.TryEnter或Semaphore.WaitOne的超时版本
性能问题:
- 保持临界区代码尽可能短
- 不要在锁内执行耗时操作(如I/O)
- 考虑使用异步同步机制(如SemaphoreSlim.WaitAsync)
异常处理:
- 确保在finally块中释放锁
- 避免锁内抛出异常导致锁泄漏
调试技巧:
- 给锁对象命名以便调试
- 使用Thread.CurrentThread.ManagedThreadId跟踪线程
- 考虑使用更高级的工具如Concurrency Visualizer
七、总结与最佳实践
在多线程编程中,选择合适的同步机制就像选择正确的工具完成工作一样重要。经过我们的比较:
- Monitor是"瑞士军刀"——简单通用但功能有限
- Semaphore是"流量警察"——擅长控制并发数量
- ReaderWriterLockSlim是"智能管家"——为读多写少场景优化
最佳实践建议:
- 从简单开始,先尝试Monitor
- 评估你的场景是读多还是写多
- 考虑是否需要限制并发数量
- 始终注意锁的释放
- 在高并发场景下进行性能测试
记住,多线程同步不是银弹,过度使用会导致性能下降。有时候,无锁数据结构或任务并行库(Task Parallel Library)可能是更好的选择。理解每种工具的优缺点,才能在正确的场景做出正确的选择。
评论