Android SystemProperties.Update方法中内存屏障与原子操作重排序的疑问
这个问题问到了Android系统属性实现中最核心的内存模型细节,我来一步步给你拆解清楚,帮你消除这个疑惑。
首先,我们先明确代码的核心合约:
The contract with readers is that whenever the dirty bit is set, an undamaged copy of the pre-dirty value is available in the dirty backup area.
简单说就是:当读者看到serial的脏位(最低位)被置1时,dirty_backup_area里一定存着完整的旧值,这是读写双方的核心约定。
接下来我们看你担心的重排问题:你怕编译器/CPU把strlcpy(pi->value, value, len + 1)重排到atomic_store_explicit(&pi->serial, serial, memory_order_relaxed)之前,导致读者在脏位还没置1的时候,读到正在修改的pi->value(也就是损坏的内容)。
为什么这里不需要额外的屏障来阻止这个重排?原因有两个:
1. 读者的乐观锁重试机制已经兜底了
先看读者的代码逻辑,它是一个循环:
for (;;) { serial = new_serial; len = SERIAL_VALUE_LEN(serial); // 根据serial的状态读backup或pi->value if (__predict_false(SERIAL_DIRTY(serial))) { prop_area* pa = contexts_->GetPropAreaForName(pi->name); memcpy(value, pa->dirty_backup_area(), len + 1); } else { memcpy(value, pi->value, len + 1); } atomic_thread_fence(memory_order_acquire); new_serial = load_const_atomic(&pi->serial, memory_order_relaxed); if (__predict_true(serial == new_serial)) { break; } // 重试前的屏障保证下一次读取的可见性 atomic_thread_fence(memory_order_acquire); }
这个循环的核心是双重检查serial的一致性:如果第一次读取serial后,数据读取过程中serial被修改了(比如Update线程置了脏位或者更新了新的serial),第二次读取的serial就会和第一次不一致,此时读者会放弃当前读到的内容,重新进入循环读取。
也就是说,哪怕Update线程的strlcpy真的被重排到atomic_store前面,读者可能在第一次循环中读到不完整的pi->value,但第二次循环会立刻发现serial已经变化,然后重新读取——这时候要么读到dirty_backup_area里的完整旧值,要么读到更新后的完整新值,不会一直停留在损坏的状态。
2. 实际硬件的重排约束进一步降低了风险
虽然C++内存模型理论上允许relaxed原子写和后续的普通写重排,但在Android支持的主流硬件架构上,这种重排几乎不会发生:
- x86架构:硬件原生保证写操作的顺序一致性(Store-Store重排被禁止),所以strlcpy的写操作绝对不会跑到atomic_store前面。
- ARM架构:虽然硬件允许Store-Store重排,但第一个
atomic_thread_fence(memory_order_release)已经是一个Store-Store屏障(对应ARM的dmb ish st指令),它保证了屏障之前的写操作(拷贝到dirty_backup_area)都完成后,屏障之后的写操作才能开始。虽然屏障之后的两个写操作(atomic_store和strlcpy)理论上可以重排,但实际中ARM CPU对写重排的限制也比较严格,尤其是对不同缓存行的写,重排的概率极低——即使发生了,也有上面的乐观锁重试机制兜底。
为什么这里用memory_order_relaxed而不是更强的顺序?
你可能会问,为什么不把atomic_store的内存顺序改成release,彻底阻止重排?其实完全没必要:
- 第一个release屏障已经保证了dirty_backup_area的写操作在atomic_store之前完成,满足了核心合约;
- 用relaxed的开销更小,而乐观锁机制已经覆盖了所有极端情况,不会影响正确性。
总结一下:Android的系统属性实现是基于乐观锁的无锁设计,它不追求绝对的指令顺序(那样会带来不必要的性能开销),而是通过读写双方的合约+重试机制来保证最终的一致性,同时利用硬件特性和内存屏障来覆盖绝大多数常见场景的正确性。
内容来源于stack exchange

