如何让类std::vector的C++自定义动态数组实现线程安全?
针对C++可扩容数组的多线程无锁读解决方案
给你几个实用的方案,都是针对扩容极少、不想每次读都加锁的场景设计的:
1. 原子指针间接层(最常用的轻量方案)
核心思路是用一个原子指针指向当前的数组缓冲区,读操作只需要原子加载这个指针,之后直接访问缓冲区元素;扩容时才加锁,完成新缓冲区分配和数据复制后,原子替换指针。
关键实现代码片段:
#include <atomic> #include <mutex> template<typename T> class LockFreeVector { private: std::atomic<T*> m_data = nullptr; size_t m_capacity = 0; std::atomic<size_t> m_size = 0; std::mutex m_expand_mutex; // 仅扩容时用 public: // 读操作:完全无锁 T& operator[](size_t idx) { // 原子加载当前缓冲区指针,memory_order_acquire保证后续读可见 T* current_data = m_data.load(std::memory_order_acquire); if (idx >= m_size.load(std::memory_order_relaxed)) { throw std::out_of_range("Index out of bounds"); } return current_data[idx]; } // 扩容操作:仅这里加锁 void reserve(size_t new_capacity) { std::lock_guard<std::mutex> lock(m_expand_mutex); if (new_capacity <= m_capacity) return; T* new_data = new T[new_capacity]; T* old_data = m_data.load(); // 复制旧数据 for (size_t i = 0; i < m_size; ++i) { new_data[i] = std::move(old_data[i]); } // 原子替换指针,memory_order_release保证之前的写对其他线程可见 m_data.store(new_data, std::memory_order_release); m_capacity = new_capacity; // 若存在长耗时读操作,需延迟释放旧内存,可搭配引用计数或epoch回收机制 delete[] old_data; } // 增删改操作略,需同步时可复用m_expand_mutex或单独加锁 };
2. 预分配足够容量(最简单的方案)
如果能提前预估业务的最大元素数量,直接在容器初始化时预分配足够的capacity,彻底禁用自动扩容逻辑。这样数组缓冲区永远不会移动,读操作完全不需要任何同步(元素的增删改仍需同步,但和扩容无关)。
示例:
CustomVector<int> vec; vec.reserve(10000); // 预分配足够容纳10000个元素的空间 // 后续操作不会触发扩容,读元素直接访问即可
这个方案零额外开销、实现最简单,但仅适合业务场景明确、容量可预估的情况。
3. 读写锁(shared_mutex)实现
用C++17提供的std::shared_mutex(或Boost的shared_mutex),读操作加共享锁(多线程可同时持有),扩容等写操作加独占锁(同一时间仅一个线程持有)。
代码示例:
#include <shared_mutex> template<typename T> class SharedVector { private: T* m_data = nullptr; size_t m_capacity = 0; size_t m_size = 0; std::shared_mutex m_mutex; public: // 读操作:加共享锁,开销远小于独占锁 const T& operator[](size_t idx) const { std::shared_lock<std::shared_mutex> lock(m_mutex); if (idx >= m_size) throw std::out_of_range("Index out of bounds"); return m_data[idx]; } // 扩容操作:加独占锁 void expand() { std::unique_lock<std::shared_mutex> lock(m_mutex); size_t new_capacity = m_capacity == 0 ? 4 : m_capacity * 2; T* new_data = new T[new_capacity]; for (size_t i = 0; i < m_size; ++i) { new_data[i] = std::move(m_data[i]); } delete[] m_data; m_data = new_data; m_capacity = new_capacity; } };
这个方案实现简单,读操作开销极低,无需处理复杂的内存回收问题,适合不想搞复杂逻辑的场景。
4. 稳定缓冲区设计(最灵活但复杂)
把容器的元数据(大小、容量)和缓冲区分离,缓冲区一旦分配就不再移动。扩容时分配新缓冲区,旧缓冲区标记为只读,后续读操作可继续访问旧缓冲区,写操作只用新缓冲区;同时用引用计数跟踪旧缓冲区的访问次数,等所有读操作完成后再释放旧内存。
这个方案读操作完全无锁,扩容也不会阻塞读,但实现复杂度较高,适合对性能要求极高的场景。
内容的提问来源于stack exchange,提问作者Zebrafish
相关产品推荐
相关产品推荐

