多线程应用中对象引用属性值同步的正确实现方式咨询
1 加锁版本的正确性与潜在问题
你提供的lock实现完全可以满足线程安全获取最新Options值的需求。
lock底层除了互斥能力外,自带完整的内存屏障语义:进入lock时触发获取屏障,保证后续读操作不会被重排到屏障之前,且会从主存拉取最新数据;退出lock时触发释放屏障,保证之前的写操作都会被同步到主存。所以完全可以避免读到过时缓存值的问题。
唯一的潜在问题是性能冗余:你的场景只是单个引用的读写,不需要互斥能力(本身引用赋值就是原子的,不会出现写一半的情况),lock的Monitor开销对于这个场景来说过高,属于能力过剩。
2 Interlocked实现的正确性
这个Interlocked版本完全符合同步要求。
所有Interlocked操作都自带全内存屏障语义,不管是读侧的Interlocked.CompareExchange还是写侧的Interlocked.Exchange,都会强制同步CPU缓存,既保证读操作拿到最新值,也保证写操作立刻同步到所有CPU核心,性能比lock实现高很多,功能上没有问题。
3 单侧同步的风险
这种只在get加同步、set不加的实现存在可见性风险,不推荐使用。
引用赋值的原子性只保证你不会读到残缺的半赋值引用,完全不保证内存可见性:不带同步的普通写入可能被编译器、CPU优化缓存在寄存器或者核心私有缓存中,不会立刻刷到主存。即使get侧加了内存屏障,最多只能保证从主存读数据,但如果写侧根本没把新值刷到主存,读线程还是会拿到过时值。
你在x86设备上难复现是因为x86属于强内存模型,普通写操作默认自带释放屏障,大部分场景下会自动刷缓存,但换到ARM架构(比如移动端、新的Windows ARM设备)上,这个问题会非常容易触发,不能因为难复现就认为没有问题。
4 Volatile方案的合理性
你最终选择的Volatile.Read/Volatile.Write方案是当前场景的最优解:
Volatile.Read自带获取内存屏障,保证读到的是最新的引用值Volatile.Write自带释放内存屏障,保证新引用立刻对其他线程可见- 不需要全内存屏障,性能比Interlocked方案更高,代码语义也更明确,刚好匹配你不可变对象引用更新的场景。
内容的提问来源于stack exchange,提问作者heyufool1

