Folly的hazard pointer实现是否存在内存屏障使用错误问题?
问题
当使用Linux上的非对称内存屏障时,Folly的hazard pointer实现可以简化为如下逻辑:
Atomic<T*> source_ptr; 1. writer/updater: (retire operation) old_ptr = source_ptr.load(); source_ptr.exchange(....); compiler barrier;==================== move old_ptr to retirement list 2. consumer/reader: (try_protect operation) ptr = source_ptr.load(); compiler barrier;==================== if (source_ptr.load() == ptr) { success; start to use ptr; } fail; retry to get source_ptr; 3. reclaim: read all retirement list; heavy memory barrier --> membarrier() call; =============== read all hazard pointers; reclaim (delete) ptr if ptr is in retirement list but not in hazard pointers.
步骤3中的membarrier()可以分别与上述步骤1、步骤2同步,但步骤1和步骤2本身之间不存在同步机制。
因此我想知道下述执行时序是否可能发生,这属于实现bug还是我遗漏了相关要点?
source ptr: 可被hazard pointer保护的内存指针 thread 1: 尝试删除source ptr的线程 thread 2: 尝试读取source ptr的消费者线程 thread 3: 执行内存回收的线程(从退休列表中释放内存) 初始状态: source ptr value = PTR_A 时间点0: 线程1(更新者): 将source ptr的值从PTR_A改为PTR_B,并将PTR_A放入退休列表(退休操作); 假设这些操作的结果对消费者线程(线程2)不可见,但对回收线程(线程3)可见,因为这里只有轻量级屏障。 时间点1: 线程3(回收线程): 读取退休列表,发现PTR_A在列表中。 调用重量级屏障`membarrier()`。 假设它先给线程2发送IPI,线程2所在的CPU处理完成。 时间点2: 线程2(消费者线程): 从source ptr读取到旧值PTR_A(新值还不可见)。 调用`try_protect`保护这个指针。 但此时这个操作对其他CPU还不可见,还是因为现在只有编译器屏障。线程2开始使用PTR_A。 时间点3: 线程3在时间点1发起的重量级屏障现在到达线程1并执行完成。 现在线程3开始收集hazard pointer列表。 但线程2在时间点2设置的hazard pointer对线程3还不可见。 所以线程3看不到这个保护记录,会判定PTR_A在退休列表中但不在hazard pointer列表中,也就是说它会开始删除PTR_A。
请问这是实现bug还是我遗漏了什么机制?
回答
你假设的执行时序不可能发生,不属于Folly hazard pointer实现的bug,是你对membarrier()的语义和hazard pointer的核心实现细节存在误解,核心原因有三点:
- 你给出的
try_protect简化逻辑遗漏了核心操作:两次source_ptr.load()结果相等判定成功后,必须先将拿到的ptr写入当前线程专属的hazard pointer原子槽位,之后才能开始使用指针。你描述的时序里跳过了这一步,导致对可见性的判断出现偏差。 - Linux
membarrier()系统调用的语义不支持你假设的执行流程:Folly使用的MEMBARRIER_CMD_PRIVATE_EXPEDITED模式下,membarrier()调用后会阻塞,直到所有当前运行该进程线程的CPU都处理完同步IPI、执行了一次全内存屏障之后才会返回。不存在你说的「先处理完线程2的IPI、再处理线程1的IPI,中间还能插入线程2的try_protect逻辑」的情况,线程3只有等所有CPU的屏障都执行完成后,才会继续执行读取hazard pointer列表的逻辑。 - 内存屏障的可见性保证完全杜绝了悬空访问的可能:
- 如果线程2的
try_protect逻辑是在membarrier()下发的IPI处理之前执行的,那么IPI带来的全内存屏障会保证线程2对hazard pointer槽位的写入,在线程3后续读取hazard pointer列表时可见,PTR_A会被判定为正在被使用,不会被回收。 - 如果线程2的
try_protect逻辑是在IPI处理之后执行的,那么此时线程1对source_ptr的修改已经通过屏障同步到所有CPU,线程2两次load拿到的只会是新的PTR_B,根本不会拿到已经退休的PTR_A,也就不会出现访问已回收内存的情况。
- 如果线程2的
内容的提问来源于stack exchange,提问作者sheeper
相关产品推荐
相关产品推荐

