std::forward_list多线程读写同一节点的线程安全性及标准规范问询
关于std::forward_list并发操作的线程安全性问题
参考代码
#include <iostream> #include <vector> #include <mutex> #include <forward_list> using std::cout; using std::endl; class Cache { // thread safe, only 1 writer, thanks to mutex int add(int val) { std::lock_guard<std::mutex> lk(mtx); if(flSize == 0) { fl.push_front(val); backIt = fl.begin(); } else { backIt = fl.insert_after(backIt,val); } ++flSize; return flSize - 1; } // allow concurrent readers (with other readers and writer) // get the element at idx into the forward linked list // the thread calling get() will never try to index past the last // index/value it pushed to list. It uses the return value from add() as // the arg to get // a.k.a get will never be called before an add() has been called, and // if add has been called N times, the index to get will be [0, N) int get(int idx) { int num = 0; auto it = fl.cbegin(); while(num < idx) { ++num; ++it; } return *it; } private: std::forward_list<int> fl; size_t flSize {0}; std::forward_list<int>::iterator backIt; std::mutex mtx; };
目标
让读取线程能读取已完全构造的链表节点。
思考
在给定约束下,代码看似线程安全,但可能依赖实现细节,存在潜在问题与假设错误,不确定是否具备编译器可移植性及未来兼容性。
技术问询
当一个线程访问std::forward_list中某节点的数据时,另一线程对该节点执行std::forward_list::insert_after操作,此场景是否线程安全?C++标准对此类场景是否提供相关指导规范?
回答
代码存在的核心问题
你的get函数通过计数遍历链表的方式,在并发插入场景下必然会出现索引错位:写入线程在遍历过程中插入新节点,会改变链表的长度与结构,导致读取线程的计数与目标节点的对应关系失效,最终读取到错误的元素。例如,读取线程要访问idx=2的节点,遍历到中间时写入线程在某个节点后插入新元素,会让读取线程的迭代器跳到新插入的节点,最终返回错误的值。
C++标准对容器并发操作的规范
C++11及以后的标准对标准容器的线程安全有明确规则:
- 多个线程同时调用容器的const成员函数是安全的,前提是没有任何线程同时修改容器。
- 若有线程执行容器结构修改操作(如插入、删除元素、改变容器大小),则其他线程不能同时进行任何读取或修改操作——除非是对完全独立的元素进行纯数据访问(不涉及容器结构相关的成员,如指针、迭代器遍历)。
针对你提到的场景:insert_after操作仅会修改目标节点的next指针(属于节点的一个独立内存位置),如果读取线程仅访问该节点的数据成员(而非next指针),从内存竞争的角度看,这两个操作针对不同内存位置,不存在数据竞争。但问题在于你的get函数依赖遍历计数,而结构修改会破坏遍历的正确性,这是逻辑上的线程不安全,而非内存竞争层面的问题。
另外,代码中flSize的修改由互斥锁保护,add返回的索引与插入操作的内存可见性由互斥锁的释放提供内存屏障保证,这部分是符合规范的,但无法解决遍历计数错位的问题。
修复建议
- 放弃计数遍历的方式,改用一个线程安全的容器(如带互斥锁保护的
std::vector)存储每个节点的迭代器或指针:每次add时,在互斥锁保护下将新节点的迭代器存入vector,get时直接通过索引获取迭代器并访问数据,避免遍历。std::forward_list的迭代器在插入操作后不会失效(除非节点被删除),因此保存迭代器是安全的。 - 若场景允许,直接改用
std::vector替代std::forward_list:vector尾部插入(无扩容时)迭代器与引用均有效,且随机访问是O(1)操作,更适合这种按索引访问的并发场景。
内容的提问来源于stack exchange,提问作者PYA
相关产品推荐
相关产品推荐

