第 7 章:并发编程与线程安全

jerry北京市2026年4月12日C# 11 次阅读 约 13 分钟
第 7 章:并发编程与线程安全

掌握 lock 的底层实现、Monitor、Semaphore、ReaderWriterLock、Interlocked 原子操作和 volatile 关键字。


7.1 lock 的底层

lockMonitor.Enter / Monitor.Exit 的语法糖:

// 你写的代码
lock (_syncObj)
{
    // 临界区
}

// 编译器生成的代码
bool lockTaken = false;
try
{
    Monitor.Enter(_syncObj, ref lockTaken);
    // 临界区
}
finally
{
    if (lockTaken)
        Monitor.Exit(_syncObj);
}

Monitor 的底层

每个对象头中有一个同步块索引(Sync Block Index),指向同步块表中的条目:

对象内存布局:
┌──────────────────────┐
│ Sync Block Index     │ ← 指向同步块表
│ Method Table Pointer │
│ 字段数据...          │
└──────────────────────┘

同步块表:
┌─────────────────────────┐
│ 拥有者线程 ID           │
│ 递归计数               │
│ 等待队列(等待的线程)   │
└─────────────────────────┘

lock 的注意事项:

// 好:使用专用的 object
private readonly object _lock = new object();
lock (_lock) { }

// 差:锁 this(外部代码也可能锁同一个对象)
lock (this) { }

// 差:锁 Type 对象(全局唯一,影响范围太大)
lock (typeof(MyClass)) { }

// 差:锁字符串(字符串驻留导致不同代码锁同一个对象)
lock ("my-lock") { }

// 差:锁值类型(装箱后每次是不同的对象,锁无效)
lock (42) { }  // 编译错误

7.2 SemaphoreSlim

控制同时访问资源的线程数量:

// 最多允许 3 个线程同时访问
private readonly SemaphoreSlim _semaphore = new(3);

async Task ProcessAsync()
{
    await _semaphore.WaitAsync();  // 获取许可(异步等待)
    try
    {
        await DoWorkAsync();
    }
    finally
    {
        _semaphore.Release();  // 释放许可
    }
}

lock vs SemaphoreSlim:

特性 lock SemaphoreSlim
并发数 1(互斥) 可配置(1-N)
异步支持 不支持 WaitAsync()
跨方法 不推荐 支持
可重入

7.3 ReaderWriterLockSlim

读多写少场景的优化锁:

private readonly ReaderWriterLockSlim _rwLock = new();
private readonly Dictionary<string, string> _cache = new();

string Read(string key)
{
    _rwLock.EnterReadLock();      // 多个读可以并发
    try { return _cache[key]; }
    finally { _rwLock.ExitReadLock(); }
}

void Write(string key, string value)
{
    _rwLock.EnterWriteLock();     // 写独占
    try { _cache[key] = value; }
    finally { _rwLock.ExitWriteLock(); }
}

7.4 Interlocked 原子操作

比 lock 更轻量的线程安全操作:

private int _counter = 0;

// 原子递增
Interlocked.Increment(ref _counter);

// 原子递减
Interlocked.Decrement(ref _counter);

// 原子加法
Interlocked.Add(ref _counter, 10);

// 原子交换
int oldValue = Interlocked.Exchange(ref _counter, 100);

// CAS(Compare And Swap)
int original = Interlocked.CompareExchange(ref _counter, 200, 100);
// 如果 _counter == 100,则设为 200,返回原始值

// 无锁模式示例:CAS 循环
int SpinAdd(ref int location, int value)
{
    int original, newValue;
    do
    {
        original = location;
        newValue = original + value;
    } while (Interlocked.CompareExchange(ref location, newValue, original) != original);
    return newValue;
}

7.5 volatile 关键字

防止编译器和 CPU 对内存访问进行重排序:

class Worker
{
    private volatile bool _shouldStop = false;

    public void DoWork()
    {
        while (!_shouldStop)  // 每次都从内存读取,不使用缓存
        {
            // 工作...
        }
    }

    public void Stop() => _shouldStop = true;
}

volatile 的作用:

  • 读操作:从主内存读取,不使用 CPU 缓存
  • 写操作:立即写入主内存
  • 防止指令重排序

volatile vs lock vs Interlocked:

场景 推荐
简单的标志位读写 volatile
单个变量的原子操作 Interlocked
复合操作(多个变量) lock

7.6 并发模式

双重检查锁定(Double-Check Locking)

class Singleton
{
    private static volatile Singleton? _instance;
    private static readonly object _lock = new();

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)           // 第一次检查(无锁)
            {
                lock (_lock)
                {
                    if (_instance == null)   // 第二次检查(有锁)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

// 更简洁的方式:Lazy<T>
private static readonly Lazy<Singleton> _lazy =
    new(() => new Singleton());
public static Singleton Instance => _lazy.Value;

生产者-消费者

var channel = Channel.CreateBounded<int>(100);

// 生产者
async Task ProduceAsync()
{
    for (int i = 0; i < 1000; i++)
    {
        await channel.Writer.WriteAsync(i);
    }
    channel.Writer.Complete();
}

// 消费者
async Task ConsumeAsync()
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        Console.WriteLine(item);
    }
}

7.7 面试要点

  1. lock 的底层实现?

    • Monitor.Enter/Exit,基于对象头的同步块索引
  2. 为什么不能 lock 值类型?

    • 值类型会装箱,每次装箱产生不同的对象,锁无效
  3. Interlocked 和 lock 的区别?

    • Interlocked 是 CPU 级别的原子操作,更轻量
    • lock 适合复合操作
  4. volatile 的作用?

    • 防止编译器/CPU 重排序,保证内存可见性
  5. 如何实现线程安全的单例?

    • Lazy<T>(推荐)或双重检查锁定

练习

  1. 使用 Interlocked 实现一个无锁计数器
  2. 使用 SemaphoreSlim 实现并发限制的 HTTP 请求
  3. 使用 ReaderWriterLockSlim 实现一个线程安全的缓存
  4. 对比 lock、SemaphoreSlim、Interlocked 的性能

← 上一章:Task 与线程池 | 下一章:Channel 与并发集合 →

评论

登录 后发表评论

暂无评论