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

C++20下能否用std::list搭配原子shared_ptr实现线程安全共享指针列表

直接拆透这里的选型坑

你现在的思路有两个核心误区,先讲清楚原理再给明确选型建议:

首先要拆清楚两个完全独立的“线程安全”要求

很多人会把这俩概念混为一谈,从根上导致选型错误:

  • 元素级原子安全:列表里每个节点存的共享指针,被多线程同时读写时不会出现数据竞争,这正是C++20的std::atomic<std::shared_ptr<Delegate>> 解决的问题。
  • 容器结构安全:对列表本身的操作,比如插入节点、删除节点、调整节点间的前后指向关系、遍历整个列表的结构,多线程同时操作不会损坏链表结构、不会产生数据竞争——这部分的安全和元素是不是原子没有半毛钱关系。

为什么std::list<std::atomic<std::shared_ptr<Delegate>>> 根本达不到你的要求

两个绕不开的硬伤:

  • 第一,标准库的std::list 本身没有任何并发保护。它的内部结构(头节点指针、每个节点的prev/next指针、全局计数的size变量)全是非原子的,只要多个线程同时执行插入、删除、splice这类修改链表结构的操作,哪怕每个节点存的元素是原子的,照样会把链表的链接关系改坏,轻则内存越界,重则直接崩溃。哪怕你靠外层包装绕开了编译报错的问题,内部的非原子指针没有保护的问题依然存在。
  • 第二,你甚至都编不过这段代码。std::atomic 类型是既不可拷贝也不可移动的(它的拷贝/移动构造、赋值运算符都是被删除的),而标准std::list 的大量操作(比如节点重排、临时元素构造、列表的拷贝/赋值)都要求元素类型是可移动、可拷贝的,你把原子类型塞进去当list的元素,编译阶段就会报一堆错。

那到底要不要从零自己写双向链表?

完全没必要一上来就造轮子,根据你的实际场景选就行:

  • 如果你是常规业务场景,对列表的并发访问量没有极端的性能要求:根本不需要自己写链表。最稳妥的做法就是给标准std::list<std::shared_ptr<Delegate>> 外面套一层读写锁,所有涉及列表结构修改的操作(增删节点、遍历修改)全部在锁的保护下执行就行。这种写法实现简单,维护成本极低,只要锁的粒度控制合理,性能足够覆盖90%以上的业务场景,而且因为所有访问都在锁的保护下,根本不需要给每个元素额外套原子,能减少冗余开销。
  • 如果你真的有极致性能要求,需要实现无锁/细粒度锁的列表:这时候才需要自己实现链表结构。自己写的时候要注意两个核心问题:一是链表节点的prev/next指针必须换成原子类型,不能用标准list的非原子指针;二是必须配套实现无锁内存回收机制(比如hazard pointer、epoch reclamation),不然删除节点的瞬间,其他正在遍历该节点的线程很容易触发use-after-free。只有在这种细粒度无锁的场景下,把节点存的std::shared_ptr<Delegate> 换成std::atomic<std::shared_ptr<Delegate>> 才有意义——毕竟你要的就是不拿全局锁就能修改单个节点指向的对象。

最后提几个容易踩的坑

  • 别觉得元素是原子的,整个容器就线程安全了:这是写并发容器最常见的入门误区,容器的内部结构保护和元素原子性是完全独立的两层。
  • 别在不需要无锁的场景强行造无锁轮子:自己实现的无锁链表很容易藏ABA问题、内存泄漏、野指针这类隐蔽bug,开发和调试成本远高于加一把锁的实现,除非压测发现锁的开销确实成了瓶颈,否则优先选带锁的薄封装。
  • 注意std::atomic<std::shared_ptr<T>> 的实际开销:很多平台的实现并不是无锁的,是带内部锁的,如果你整个列表的操作本来就有全局锁保护,再给每个元素套一层原子纯属冗余。

内容的提问来源于stack exchange,提问作者monamimani

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.29 13:15:04