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

掌握 lock 的底层实现、Monitor、Semaphore、ReaderWriterLock、Interlocked 原子操作和 volatile 关键字。
7.1 lock 的底层
lock 是 Monitor.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 面试要点
-
lock 的底层实现?
- Monitor.Enter/Exit,基于对象头的同步块索引
-
为什么不能 lock 值类型?
- 值类型会装箱,每次装箱产生不同的对象,锁无效
-
Interlocked 和 lock 的区别?
- Interlocked 是 CPU 级别的原子操作,更轻量
- lock 适合复合操作
-
volatile 的作用?
- 防止编译器/CPU 重排序,保证内存可见性
-
如何实现线程安全的单例?
- Lazy<T>(推荐)或双重检查锁定
练习
- 使用 Interlocked 实现一个无锁计数器
- 使用 SemaphoreSlim 实现并发限制的 HTTP 请求
- 使用 ReaderWriterLockSlim 实现一个线程安全的缓存
- 对比 lock、SemaphoreSlim、Interlocked 的性能
评论
登录 后发表评论
暂无评论