一、多线程并发冲突的那些事儿

咱们程序员在写C#程序时,经常会遇到多线程并发的问题。比如,你有一个在线购物系统,100个人同时抢购最后一件商品,如果不处理好线程冲突,可能就会超卖——这件商品被卖了100次!这显然不合理。

多线程并发冲突的核心问题是:多个线程同时读写共享资源。比如全局变量、静态字段、文件、数据库记录等。举个简单例子:

// 技术栈:C# (.NET 6)
// 一个典型的线程冲突示例
public class Counter
{
    private int _count = 0;

    // 多个线程同时调用这个方法会导致计数错误
    public void Increment()
    {
        _count++; // 这里不是原子操作!
    }

    public int GetCount() => _count;
}

你可能觉得_count++只是一行代码,不会有问题。但实际上,它会被编译成多条CPU指令:读取当前值、加1、写回新值。如果两个线程同时执行这行代码,可能会出现两个线程都读到相同的初始值(比如10),各自加1后写回11,最终结果应该是12,但实际上却是11。

二、解决并发冲突的四大法宝

1. 锁(lock)

锁是最简单粗暴的解决方案,就像厕所门上的"有人"标志,一次只允许一个线程进入。

// 使用lock的线程安全计数器
public class SafeCounter
{
    private int _count = 0;
    private readonly object _lock = new object(); // 专门用于锁的对象

    public void Increment()
    {
        lock (_lock) // 关键代码加锁
        {
            _count++;
        }
    }

    public int GetCount()
    {
        lock (_lock)
        {
            return _count;
        }
    }
}

注意事项

  • 锁对象应该是私有的,避免外部代码也能锁它
  • 锁的范围要尽量小,否则会影响性能
  • 不要锁字符串(因为字符串可能被驻留)

2. 互斥体(Mutex)

Mutex比lock更重量级,可以跨进程使用。比如防止程序多开:

// 使用Mutex确保程序单实例运行
static void Main()
{
    using var mutex = new Mutex(true, "MyAppUniqueMutex", out bool createdNew);
    
    if (!createdNew)
    {
        Console.WriteLine("程序已经在运行了!");
        return;
    }
    
    // 正常程序逻辑...
}

3. 信号量(Semaphore)

信号量就像游乐园的旋转门,一次允许N个线程通过:

// 限制最多5个线程同时访问资源
private static Semaphore _pool = new Semaphore(5, 5);

void AccessResource()
{
    _pool.WaitOne(); // 等待许可
    try
    {
        // 访问共享资源...
    }
    finally
    {
        _pool.Release(); // 释放许可
    }
}

4. 原子操作(Interlocked)

对于简单的数值操作,使用原子操作性能最好:

// 使用Interlocked实现线程安全计数
public class AtomicCounter
{
    private int _count = 0;

    public void Increment() => Interlocked.Increment(ref _count);
    
    public int GetCount() => Interlocked.CompareExchange(ref _count, 0, 0);
}

三、高级并发控制技术

1. 读写锁(ReaderWriterLockSlim)

当读多写少时,读写锁可以大幅提升性能:

private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();

// 读取数据(允许多个线程同时读)
public string ReadData()
{
    _rwLock.EnterReadLock();
    try
    {
        return _data; // 假设_data是共享数据
    }
    finally
    {
        _rwLock.ExitReadLock();
    }
}

// 修改数据(一次只允许一个线程写)
public void WriteData(string newData)
{
    _rwLock.EnterWriteLock();
    try
    {
        _data = newData;
    }
    finally
    {
        _rwLock.ExitWriteLock();
    }
}

2. 并发集合(Concurrent Collections)

.NET提供了现成的线程安全集合:

// 使用ConcurrentDictionary实现线程安全的缓存
var cache = new ConcurrentDictionary<string, object>();

// 添加或更新缓存项(线程安全)
cache.AddOrUpdate("key", 
    key => GetExpensiveValue(key), // 如果key不存在,调用这个函数获取值
    (key, oldValue) => GetUpdatedValue(key, oldValue)); // 如果key存在,调用这个函数更新值

// 安全读取
if (cache.TryGetValue("key", out var value))
{
    // 使用value...
}

四、实战:实现一个线程安全的日志系统

让我们用所学知识实现一个实用的线程安全日志系统:

public class ThreadSafeLogger
{
    private readonly ConcurrentQueue<string> _logQueue = new ConcurrentQueue<string>();
    private readonly SemaphoreSlim _logSemaphore = new SemaphoreSlim(1);
    private readonly string _logFilePath;
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();
    
    public ThreadSafeLogger(string logFilePath)
    {
        _logFilePath = logFilePath;
        // 启动后台写入线程
        Task.Run(ProcessLogQueue);
    }
    
    // 添加日志(线程安全)
    public void Log(string message)
    {
        _logQueue.Enqueue($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
        _logSemaphore.Release(); // 通知有新的日志
    }
    
    // 后台处理日志队列
    private async Task ProcessLogQueue()
    {
        while (!_cts.IsCancellationRequested)
        {
            await _logSemaphore.WaitAsync(_cts.Token);
            
            // 批量处理队列中的日志
            var batch = new List<string>();
            while (_logQueue.TryDequeue(out var logEntry))
            {
                batch.Add(logEntry);
            }
            
            if (batch.Count > 0)
            {
                await File.AppendAllLinesAsync(_logFilePath, batch);
            }
        }
    }
    
    public void Dispose()
    {
        _cts.Cancel();
        _cts.Dispose();
    }
}

这个日志系统有三大特点:

  1. 使用ConcurrentQueue实现线程安全的日志入队
  2. 使用SemaphoreSlim实现高效的通知机制
  3. 后台线程批量写入磁盘,减少IO操作

五、技术选型与性能考量

选择并发控制技术时,要考虑:

  1. 锁粒度:锁的范围越小越好
  2. 访问模式:读多写少用读写锁,频繁小操作用原子操作
  3. 性能影响:锁会阻塞线程,影响吞吐量
  4. 死锁风险:避免嵌套锁,按固定顺序获取多个锁

六、常见陷阱与最佳实践

  1. 死锁:A等B,B等A,程序卡死

    • 解决方案:使用Monitor.TryEnter设置超时
  2. 锁竞争:太多线程争抢同一个锁

    • 解决方案:减小锁粒度,使用无锁数据结构
  3. 线程饥饿:某些线程一直得不到执行机会

    • 解决方案:使用公平锁,限制最大并发数

记住:多线程调试比单线程难10倍,所以一定要:

  • 编写线程安全的单元测试
  • 使用Debug.WriteLine输出线程信息
  • 在关键位置添加断言检查

七、总结

处理多线程并发就像管理一个繁忙的餐厅:

  • 锁就像包间门锁,一次只允许一桌客人
  • 信号量就像大厅座位,控制同时用餐人数
  • 原子操作就像自动售货机,无需服务员

在C#中,我们有丰富的工具应对各种并发场景。关键是要理解每种技术的适用场景,避免过度设计。记住:最简单的解决方案往往是最好的