Rust中SeqLock实现的未定义行为(UB)及可行性探讨
SeqLock实现的技术疑问与解答
use std::alloc::Layout; use std::sync::atomic::{fence, AtomicUsize, Ordering}; use std::sync::Arc; use std::thread::yield_now; const LENGTH: usize = size_of::<usize>(); #[derive(Clone, Copy)] struct SharedBuffer(*mut u8); unsafe impl Sync for SharedBuffer {} unsafe impl Send for SharedBuffer {} fn main() { // for the sake of brevity allocate 8 byte array, but // this can be generic over any allocation size let alloc = unsafe { std::alloc::alloc(Layout::from_size_align(LENGTH, 1).unwrap()) }; let store = SharedBuffer(alloc); let sequence1 = Arc::new(AtomicUsize::new(0)); let sequence2 = sequence1.clone(); std::thread::spawn(move || writer(sequence1, store)); let _ = std::thread::spawn(move || reader(sequence2, store)).join(); } fn writer(sequence: Arc<AtomicUsize>, store: SharedBuffer) { loop { // signal other thread that we are about to write let n = sequence.fetch_add(1, Ordering::AcqRel).to_le_bytes(); unsafe { store.0.copy_from_nonoverlapping(n.as_ptr(), LENGTH) }; // signal the thread that we are done writing sequence.fetch_add(1, Ordering::Release); yield_now(); } } fn reader(sequence: Arc<AtomicUsize>, store: SharedBuffer) { let mut buffer = [0; LENGTH]; loop { loop { // check whether writer thread is accessing the memory let before = sequence.load(Ordering::Acquire); if before % 2 != 0 { // retry if write is in progress yield_now(); continue; } let ptr = buffer.as_mut_ptr(); unsafe { ptr.copy_from_nonoverlapping(store.0, LENGTH) }; fence(Ordering::Release); let after = sequence.load(Ordering::Acquire); // if writer thread managed to modify the data while // we were reading it, discard the data and retry if after != before { continue; } assert_eq!(usize::from_le_bytes(buffer), after - 2); break; } yield_now(); } }
这段代码尝试用类SeqLock机制在读写线程间共享内存,设计思路是:明知存在会引发**未定义行为(UB)**的数据竞争,但试图通过检测竞争并重试读取,实现读线程无阻塞的乐观读取。当前代码能在多个主流平台和编译器版本下编译运行,但UB的存在意味着程序行为不可预测,当前正常运行不代表在所有环境下都能工作,本质存在安全问题。
技术疑问解答
1. 是否意味着在Rust、C++这类语言中,无法实现针对任意大内存区域的SeqLock?
不是无法实现,但必须严格遵循语言的内存模型,不能直接放任数据竞争。
传统SeqLock的核心是允许读操作与写操作并发,通过序列计数器验证读取的完整性,但Rust/C++的内存模型中,对非原子共享内存的并发读写会直接触发数据竞争导致的UB——即使后续检测到并重试,读取撕裂数据的动作本身已经违反了语言规范,可能导致编译器优化出逻辑错误、程序崩溃或其他不可预测的结果。
要合法实现任意大内存的SeqLock,需要:
- 用原子变量维护序列计数器,确保其操作的原子性和内存顺序;
- 配合内存屏障(fence)阻止编译器和硬件的指令重排,保证读写操作的可见性和顺序性;
- 在unsafe代码中严格控制共享数据的访问,确保当序列值验证通过时,读取到的数据是完整一致的(这依赖目标硬件的内存模型特性,比如x86的强有序性、ARM需要额外的屏障指令);
- 一旦检测到序列值变化,立即丢弃无效数据并重试,绝不使用可能撕裂的脏数据。
示例代码的问题在于,它直接对非原子共享内存进行并发读写,没有采取足够的措施避免UB,本质是依赖编译器和硬件的非标准行为,而非合法的语言特性。
2. 若针对特定架构使用汇编编写该算法,是否能保证其正确性?
针对特定架构用汇编编写SeqLock,可以保证在该架构下的正确性,但不具备跨架构的可移植性。
原因在于:
- 汇编可以直接控制内存访问的顺序,插入符合目标架构要求的内存屏障指令(比如x86的
mfence、ARM的dmb),完全遵循架构的内存模型规范; - 可以直接指定内存访问的方式,避免编译器优化导致的指令重排或数据撕裂;
- 但不同架构的内存模型差异极大(比如x86是强有序架构,ARM是弱有序架构),针对某一架构编写的汇编SeqLock在其他架构上可能完全失效,甚至触发硬件层面的数据不一致问题。
即使是汇编实现,也需要严格遵循目标架构的内存模型规则,否则仍然会出现逻辑错误。
内容的提问来源于stack exchange,提问作者Babur Makhmudov
相关产品推荐
相关产品推荐

