多线程安全的三大条件
总结来说,多线程安全主要要满足 三大条件:
原子性(Atomicity)
操作不可分割,要么全部成功,要么全部失败。[1]
1 | int x = 0; |
可见性 (Visibility)
一个线程对共享变量的修改,另一个线程能够立即看到。
CPU 会做缓存[1]和指令重排(跟有序性也有很大关系),导致线程 A 改了变量,线程 B 可能“看不到”。[2]
需要内存屏障(memory barrier)[2]、volatile 或锁来保证。[3]
1 | volatile bool flag = false; |
有序性 (Ordering)
程序执行顺序符合预期。
编译器,JIT和CPU,可能会重排指令,导致代码执行顺序和书写顺序不一致。[4]
内存屏障、锁等可以限制重排。
1 | class Singleton { |
这个时候可以给 instance 添加 volatile,或者是用 Lazy<T>(线程安全的初始化对象)[5]
总结
在多线程编程里,原子性(Atomicity)、有序性(Ordering)、可见性(Visibility) 经常是相辅相成的——它们不是独立孤立的,而是互相依赖来保证线程安全。
脚注
[1] 现代的 cpu 大多有多级缓存,比如 L1,L2和L3,RAM,这么做是为了减少访存的时间。
| 缓存层级 | 特点 |
|---|---|
| L1 Cache | CPU 核心私有,最小、最快 |
| L2 Cache | CPU 核心私有,稍大,速度略慢 |
| L3 Cache | 共享缓存,多核心共享,速度比 L1/L2 慢,但比内存快 |
| 主存 (RAM) | 所有核心共享,访问最慢 |
一般在cpu核心读操作时先从最近的缓存找,如果能命中缓存就直接读,不会再访问其他缓存。写操作时先写到缓存再延迟写回主存(根据cpu决定,通常也会写入 L2/L3,写主存很慢,所以这个操作是延迟的)。
[2] 内存屏障是编译器和CPU提供的用来限制指令的重排序和控制缓存可见性的指令/机制。
编译器为了性能可能会做指令重排,寄存器缓存。
为了避免这种优化导致问题,编译器会提供屏障语义:
比如编译器会在volatile变量前后插入内存屏障,保证可见和有序。
比如 lock,编译器会在周围插入 full fence(禁止编译器和cpu的重排序,同时保证读写可见性)。
CPU同样可能会为了性能去乱序执行,写缓冲区延迟写回等。
为了避免这种优化导致问题,cpu有专门的指令:
x86 有 mfence、lfence、sfence
ARM 有 dmb、dsb
这些指令就是内存屏障。
Microsoft Learn 的
Interlocked.IncrementAPI 文档说明,该方法会递增指定变量并以原子操作存储结果,适合支撑单变量 read-modify-write 的原子性边界:https://learn.microsoft.com/en-us/dotnet/api/system.threading.interlocked.increment。 ↩︎Microsoft Learn 的 C#
volatile参考把volatile读写描述为 acquire / release 语义,并提醒它不保证所有线程观察到完全相同的全局顺序,因此这里的“看不到”更精确地说是缺少同步规则时没有跨线程可见性保证:https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile。 ↩︎Microsoft Learn 的
Thread.MemoryBarrier文档说明内存屏障会限制屏障前后的内存访问重排;C#lock文档说明lock会进入互斥临界区并转换为Monitor.Enter/Monitor.Exit结构;volatile文档则说明字段级 acquire / release 语义。参见 https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.memorybarrier、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/lock、https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/volatile。 ↩︎Thread.MemoryBarrier官方文档明确说,屏障之前的内存访问不能被重排到屏障之后,屏障之后的内存访问也不能被重排到屏障之前;这正是有序性讨论中的底层边界:https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.memorybarrier。 ↩︎Microsoft Learn 的 Lazy initialization 指南说明,
Lazy<T>默认创建线程安全对象,常用于延迟初始化和单例式对象发布:https://learn.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization。 ↩︎