You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C++多线程中用原子类型减少互斥锁的可行性及内存序选择

多线程C++代码优化咨询

我把多线程C++代码按访问规则分为三类:

  1. 无互斥保护:仅操作局部、thread_local变量,以及多线程期间不会变更的全局变量
  2. shared_lock保护:仅访问不可修改的全局数据结构
  3. 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互斥),无需再切换锁类型,逻辑大幅简化。

二、潜在问题

需要注意几个关键点:

  1. MyStruct其他字段的线程安全:如果MyStruct的其他字段存在多线程修改场景,必须确保这些修改符合你的三类访问规则——要么用unique_lock保护,要么改成原子类型。若其他字段只读,则当前方案无问题。
  2. 计数器溢出风险:uint64_t原子递增溢出后,会按无符号整数规则回绕到0,这是C++标准允许的行为,但如果业务逻辑依赖计数器不溢出,需要额外处理。
  3. 与其他操作的同步性:若有其他线程需要读取计数器并依赖其与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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 13:33:15