C++11无锁保护变量的乐观并发实现方法咨询
在C++11中用乐观并发实现无锁受保护变量
嘿,我完全懂你现在的困惑——想在C11里搞无锁的乐观并发,已经摸到了“最后修改ID”的思路,但卡在具体实现上对吧?其实这个方向非常靠谱,乐观并发的核心就是假设冲突很少,先操作本地副本,最后验证有没有被其他线程改动,没冲突就提交,有冲突就重试。结合C11的原子操作,咱们可以把这个思路落地成一个实用的模板类。
核心思路拆解
你提到的“复制最后修改ID→修改值→比较ID”正是乐观锁的标准流程,对应到C++11里要解决两个关键问题:
- 如何保证修改ID(版本号)和受保护值的操作线程安全?
- 如何避免ABA问题(比如ID被修改两次后回到原值,导致误判无冲突)?
解决办法很简单:把值和版本号打包成一个结构体,用std::atomic来封装这个结构体——这样我们就能用原子比较交换操作(CAS),原子性地完成“验证版本号+更新值和版本号”的动作,同时用递增的版本号彻底避免ABA问题。
具体代码实现
下面是一个通用的模板类,支持任意可平凡复制的类型(如果需要支持非平凡类型,我们后面再聊变通方案):
#include <atomic> #include <functional> #include <thread> template<typename T> class OptimisticVariable { private: // 把值和版本号打包成原子结构体 struct Data { T value; uint64_t version; // C++11要求原子类型可平凡复制,所以默认构造/赋值即可 Data() : value(), version(0) {} Data(const T& v, uint64_t ver) : value(v), version(ver) {} }; std::atomic<Data> data_; public: // 构造函数:初始化值和版本号 explicit OptimisticVariable(const T& initial_value) : data_(Data(initial_value, 0)) {} // 读操作:乐观读取,确保读到的是一致的快照 T read() const { Data current; do { // 先加载当前的data(值+版本号) current = data_.load(std::memory_order_acquire); // 复制值到本地 T temp = current.value; // 再次加载data,验证版本号是否没变——没变说明读取期间没被修改 if (data_.load(std::memory_order_acquire).version == current.version) { return temp; } // 如果版本号变了,说明期间有其他线程修改,重试 } while (true); } // 写操作:接受一个修改函数,尝试原子提交修改 // 返回true表示修改成功,false表示冲突需要重试 template<typename Func> bool modify(Func&& modify_func) { // 加载当前的data快照 Data current = data_.load(std::memory_order_acquire); Data new_data = current; // 在本地副本上执行修改逻辑 modify_func(new_data.value); // 版本号递增(彻底避免ABA问题) new_data.version++; // 原子比较交换:如果当前data的版本号和我们加载的一致,就替换成新的data // memory_order_release确保修改对其他线程可见,acquire保证后续读取能看到最新值 return data_.compare_exchange_weak( current, new_data, std::memory_order_release, std::memory_order_acquire ); } };
怎么用这个类?
举个简单的例子,用它来保护一个整数变量:
int main() { OptimisticVariable<int> counter(0); // 修改值:如果冲突就重试 while (!counter.modify([](int& val) { val += 1; })) { // 冲突时可以做些轻量操作,比如yield,或者直接空循环重试 std::this_thread::yield(); } // 读取值 int current_val = counter.read(); return 0; }
关键细节说明
- 内存顺序:用
std::memory_order_acquire和std::memory_order_release是为了保证线程间的可见性——确保一个线程的修改能被其他线程看到,同时避免不必要的内存屏障开销。 - ABA问题解决:版本号是严格递增的,哪怕值变回原来的状态,版本号也不一样,CAS操作会正确识别出冲突。
- 非平凡类型的变通:如果你的
T是有复杂构造/析构的类型,std::atomic<Data>可能不支持(C++11对原子自定义类型的限制比较严)。这时候可以用std::shared_ptr包装值,版本号递增时换一个新的shared_ptr,读操作只要拿到同一个shared_ptr就代表读取的是一致的快照。 - 适用场景:乐观并发只适合冲突概率很低的场景——如果线程冲突频繁,重试会导致性能急剧下降,这时候不如用传统的互斥锁更高效。
内容的提问来源于stack exchange,提问作者Martel
相关产品推荐
相关产品推荐

