一、多线程同步的必要性

在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更复杂

五、三种同步机制的比较与选择指南

现在我们已经了解了这三种同步机制,让我们从几个维度进行比较:

  1. 性能:

    • Monitor:中等,适合短临界区
    • Semaphore:中等,取决于许可数量
    • ReaderWriterLockSlim:读多写少场景下性能最佳
  2. 使用复杂度:

    • Monitor:最简单
    • Semaphore:中等
    • ReaderWriterLockSlim:最复杂
  3. 适用场景:

    • Monitor:简单的排他访问
    • Semaphore:资源池、限流
    • ReaderWriterLockSlim:配置管理、缓存
  4. 线程关系:

    • Monitor:同一线程可重入
    • Semaphore:不关心线程关系
    • ReaderWriterLockSlim:支持锁升级

选择建议:

  • 如果你只需要简单的互斥访问,用Monitor就够了
  • 如果需要限制并发数量,选择Semaphore
  • 如果是读多写少的场景,ReaderWriterLockSlim是最佳选择
  • 对于跨进程同步,Semaphore是唯一选择

六、实际应用中的注意事项

在使用这些同步机制时,有几个常见的坑需要注意:

  1. 死锁风险:

    • 避免嵌套获取多个锁
    • 按固定顺序获取多个锁
    • 使用Monitor.TryEnter或Semaphore.WaitOne的超时版本
  2. 性能问题:

    • 保持临界区代码尽可能短
    • 不要在锁内执行耗时操作(如I/O)
    • 考虑使用异步同步机制(如SemaphoreSlim.WaitAsync)
  3. 异常处理:

    • 确保在finally块中释放锁
    • 避免锁内抛出异常导致锁泄漏
  4. 调试技巧:

    • 给锁对象命名以便调试
    • 使用Thread.CurrentThread.ManagedThreadId跟踪线程
    • 考虑使用更高级的工具如Concurrency Visualizer

七、总结与最佳实践

在多线程编程中,选择合适的同步机制就像选择正确的工具完成工作一样重要。经过我们的比较:

  • Monitor是"瑞士军刀"——简单通用但功能有限
  • Semaphore是"流量警察"——擅长控制并发数量
  • ReaderWriterLockSlim是"智能管家"——为读多写少场景优化

最佳实践建议:

  1. 从简单开始,先尝试Monitor
  2. 评估你的场景是读多还是写多
  3. 考虑是否需要限制并发数量
  4. 始终注意锁的释放
  5. 在高并发场景下进行性能测试

记住,多线程同步不是银弹,过度使用会导致性能下降。有时候,无锁数据结构或任务并行库(Task Parallel Library)可能是更好的选择。理解每种工具的优缺点,才能在正确的场景做出正确的选择。