基于原子基元实现类RWLock基础结构的问题咨询
偏向写操作的读写锁实现问题分析与修复
原需求梳理
- 支持高读线程竞争,偏向写操作:写线程发起后,阻塞所有后续读线程直至写操作完成
- 环境限制:无内存分配器,仅允许使用栈变量与静态变量
- 特殊逻辑:已有写线程在修复数据结构时,新写线程无需执行任何操作
原实现的核心缺陷
写线程等待条件逻辑错误
原代码通过active_thread_counter.fetch_sub(writer_sign_num)标记写请求,随后等待计数器等于-writer_sign_num。但实际发起写请求时,若存在N个活跃读线程,计数器会变为N - writer_sign_num,而非-writer_sign_num。这会导致写线程必须等待所有读线程完全退出才能继续,虽符合“等待已有读线程完成”的逻辑,但计数器设计极易引发竞态与误解。多写线程竞态导致无效写操作
原代码仅判断发起写请求时是否已有写线程等待,但当一个写线程完成修复后,计数器回到0,后续写线程会重新发起写请求——即使数据结构已无需修复,违背了“已有写线程修复后新写线程无需执行”的核心需求。读线程数量溢出风险
writer_sign_num取值为0x100_0000_0000,若活跃读线程数量超过i64::MAX - writer_sign_num,计数器会溢出为负数,导致读线程被错误阻塞。
优化后的实现方案
基于原需求与环境限制,采用单原子变量+位标记的设计,逻辑清晰且无溢出风险:
use std::sync::atomic::{AtomicI64, Ordering}; use std::time::Duration; // 静态原子锁:最高位标记写请求,低63位记录活跃读线程数 static LOCK: AtomicI64 = AtomicI64::new(0); const WRITE_FLAG: i64 = 1 << 63; // i64最高位作为写请求标记 unsafe fn write() { // 尝试设置写请求标记,若已存在则直接返回(已有写线程在修复) let prev = LOCK.fetch_or(WRITE_FLAG, Ordering::SeqCst); if (prev & WRITE_FLAG) != 0 { return; } // 等待所有活跃读线程退出(低63位变为0) while (LOCK.load(Ordering::SeqCst) & !WRITE_FLAG) != 0 { std::thread::sleep(Duration::new(0, 1000)); } // -------------------------- // 此处执行数据结构修复操作 // -------------------------- // 清除写请求标记,允许后续读线程进入 LOCK.fetch_and(!WRITE_FLAG, Ordering::SeqCst); } unsafe fn sign_in() { loop { let current = LOCK.load(Ordering::SeqCst); // 若存在写请求,等待 if (current & WRITE_FLAG) != 0 { std::thread::sleep(Duration::from_nanos(2550)); continue; } // 尝试增加读线程计数 let prev = LOCK.fetch_add(1, Ordering::SeqCst); // 若增加计数前已出现写请求,撤销操作并等待 if (prev & WRITE_FLAG) != 0 { LOCK.fetch_sub(1, Ordering::SeqCst); std::thread::sleep(Duration::from_nanos(2550)); continue; } // 成功进入读操作 return; } } unsafe fn sign_out() { // 减少读线程计数 LOCK.fetch_sub(1, Ordering::SeqCst); }
方案优势
- 偏向写的语义明确:写线程设置标记后,后续读线程立即阻塞,符合需求
- 多写线程逻辑正确:仅第一个设置写标记的线程会执行修复,其他线程直接返回
- 无溢出风险:低63位可支持最多
2^63-1个活跃读线程,完全覆盖高读竞争场景 - 符合环境限制:仅使用静态原子变量,无需内存分配
内容的提问来源于stack exchange,提问作者Lantanar
相关产品推荐
相关产品推荐

