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
相关产品推荐
相关产品推荐

