C++ shared_ptr优化结构体拷贝时的竞态规避与拷贝行为疑问
问题1:无锁更新shared_ptr是否会出现半新半旧的竞态,如何规避
会出现明确的数据竞态,无锁读写同一个std::shared_ptr实例属于C++标准定义的未定义行为,完全可能读到撕裂的半新半旧指针。
不少人存在认知误区:觉得shared_ptr的引用计数是原子实现,就天生支持多线程无锁读写。这个结论只适用于「多个独立的shared_ptr实例指向同一块堆对象,各自增减引用计数」的场景,不适用于当前「多个线程同时读写同一个shared_ptr类成员变量」的场景。
shared_ptr内部本质存储两个指针:一个指向实际的DataModel::MZMap数据对象,一个指向存储引用计数、删除器的控制块,64位系统下两个指针合计占16字节,主流消费级硬件没有单条原子指令能完成16字节值的原子读写,无锁并发访问时完全可能出现「读到的控制块属于新指针、数据指针还是旧指针」的撕裂状态,轻则读错数据,重则直接触发段错误崩溃。
可选的规避方案成本都极低,完全不会回到之前高CPU开销的状态:
- 优先继续用你现有的
std::unique_lock<std::mutex>即可,优化后锁的粒度可以压到极小:锁内仅执行shared_ptr的拷贝、赋值操作,全程不涉及MZMap结构体的拷贝,单次锁占用只有几十纳秒,比之前锁内做全量深拷贝的开销低3~4个数量级,在每秒几十次的调用频率下锁开销可以完全忽略,是兼容性最好、最不容易写错的方案。 - 如果确实想消除锁开销,可以将成员类型改为C++20标准化的
std::atomic<std::shared_ptr<DataModel::MZMap>>,通过load()、store()接口做原子读写,无需加锁即可保证不会读到撕裂的指针值。
无论选哪种方案,更新逻辑必须严格遵守COW(写时复制)原则:更新数据时绝对不要直接修改当前shared_ptr指向的MZMap对象,必须先构造一个全新的MZMap实例,把所有新数据填充完成、生成完整的新shared_ptr之后,再一次性替换成员指针。否则就算指针读写是安全的,读者读取MZMap字段时写线程在原地修改对象内容,依然会出现数据竞态。
问题2:按值返回shared_ptr时是否会触发MZMap结构体拷贝
不会触发任何MZMap结构体的拷贝,仅会执行shared_ptr本身的拷贝,对应引用计数原子自增。
具体到p3 = a->assign();(assign()即调整后按值返回shared_ptr的getMZMap方法)的执行流程:
- 方法内部在锁保护/原子读的约束下,生成成员mzMap的shared_ptr副本:这一步仅复制shared_ptr内部的两个指针值,同时原子增加控制块的引用计数,全程不会访问MZMap结构体的任何成员,不存在深拷贝逻辑。
- 方法返回的临时shared_ptr赋值给
p3时,依然只是shared_ptr的拷贝/移动操作,仅操作引用计数,不会触碰MZMap实例。
整个流程中MZMap实例从头到尾只有创建时的那一份,直到最后一个指向它的shared_ptr析构时才会被释放,完全规避了之前全量深拷贝带来的高CPU开销。
内容的提问来源于stack exchange,提问作者liv2hak
相关产品推荐
相关产品推荐

