C++多线程中用原子类型减少互斥锁的可行性及内存序选择
多线程C++代码优化咨询
我把多线程C++代码按访问规则分为三类:
- 无互斥保护:仅操作局部、thread_local变量,以及多线程期间不会变更的全局变量
- shared_lock保护:仅访问不可修改的全局数据结构
- unique_lock保护:可访问并修改(含破坏性修改)全局数据结构
目前全局仅用一个shared_mutex,但现有代码里存在频繁重锁来递增计数器的冗余逻辑:
struct MyStruct { uint64_t counter; //some other fields }; unordered_map<uint64_t, MyStruct> map; shared_mutex mutex; void someFunction(uint64_t id) { shared_lock lock(mutex); auto it = map.find(id); if(it == map.end()) return; MyStruct &s = it->second; uint64_t counterValue = s.counter; doSomeWork(counterValue);//old value of counter is used mutex.unlock_shared(); mutex.lock(); //if we don't find id in map, it means that s was deleted during lock transition //id is never reused for a different struct if(map.find(id) == map.end()) return; s.counter += 1; mutex.unlock(); mutex.lock_shared(); if(map.find(id) == map.end()) return; doSomeMoreWork();//counter is not used anymore }
我想把计数器改成atomic<uint64_t>来简化逻辑,优化后的代码如下:
struct MyStruct { atomic<uint64_t> counter; //some other fields }; unordered_map<uint64_t, MyStruct> map; shared_mutex mutex; void someFunction(uint64_t id) { shared_lock lock(mutex); auto it = map.find(id); if(it == map.end()) return; MyStruct &s = it->second; uint64_t counterValue = s.counter.fetch_add(1); doSomeWork(counterValue);//old value of counter is used doSomeMoreWork();//counter is not used anymore }
由于我对原子类型缺乏使用经验,想请教:
- 这个优化方案是否可行?
- 存在哪些潜在问题?
fetch_add应该选择哪种内存序?可选值:memory_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_release、memory_order_acq_rel、memory_order_seq_cst
问题解答
一、优化方案的可行性
这个方案完全可行,而且是这类场景下的典型优化:
- 原代码核心逻辑是读取计数器旧值→用旧值执行工作→递增计数器,
fetch_add原子操作正好能原子完成「读取旧值+递增」的动作,完美匹配需求。 - 原代码的重锁逻辑本质是保证计数器递增的线程安全,改用原子类型后,
shared_lock已经能保证MyStruct对象不会在操作期间被销毁(删除操作需要unique_lock,与shared_lock互斥),无需再切换锁类型,逻辑大幅简化。
二、潜在问题
需要注意几个关键点:
MyStruct其他字段的线程安全:如果MyStruct的其他字段存在多线程修改场景,必须确保这些修改符合你的三类访问规则——要么用unique_lock保护,要么改成原子类型。若其他字段只读,则当前方案无问题。- 计数器溢出风险:
uint64_t原子递增溢出后,会按无符号整数规则回绕到0,这是C++标准允许的行为,但如果业务逻辑依赖计数器不溢出,需要额外处理。 - 与其他操作的同步性:若有其他线程需要读取计数器并依赖其与
MyStruct其他字段的一致性,需确保这些读取操作也在shared_lock保护下,或通过内存序保证同步。比如线程用unique_lock修改MyStruct其他字段时,shared_lock已能保证操作同步,无需额外内存序约束。
三、内存序的选择
针对当前场景,推荐使用**memory_order_relaxed**,理由如下:
- 你的场景中,
fetch_add仅需原子完成「读旧值+递增」,不需要和其他内存操作做同步:shared_lock已保证MyStruct对象的存在性,也保证了对MyStruct其他字段访问的同步性。doSomeWork仅使用读取到的旧计数器值,无需和其他线程操作有内存可见性约束;doSomeMoreWork不使用计数器,也无需同步。
- 其他内存序开销更高:
memory_order_seq_cst是默认值,会带来全序同步的额外开销,完全没必要。memory_order_acq_rel、release、acquire这类内存序主要用于跨线程依赖同步,比如修改数据后让其他线程可见,但你的场景里shared_lock已承担这个角色。memory_order_consume用于指针依赖的同步,此处不适用。
如果未来扩展逻辑,比如计数器递增需要和MyStruct其他字段的修改同步,再根据具体需求调整内存序,但当前场景下memory_order_relaxed是最优选择。
内容的提问来源于stack exchange,提问作者eX_ploit
相关产品推荐
相关产品推荐

