C++移动语义实战:从深拷贝陷阱到STL容器性能优化
2026/10/24 4:33:48
Obstruction-free):lock-free):cas等原子操作wait-free):exchange等原子操作队列一般涉及到生产者和消费者,需要根据生产者和消费者的个数,来设计队列
SPSCacquire,release内存序即可MPSCSPMCMPMCvolatile,内存屏障,原子操作之间的关系volatile:std::atomic)std::atomic_thread_fence):解决可见性(同步性)与顺序性std::atomic_thread_fenceLoad-Load屏障:确保屏障前的读操作先于屏障后的读操作Store-Store屏障:确保屏障前的写操作先于屏障后的写操作Load-Store屏障:确保屏障前的读操作先于屏障后的写操作Store-Load屏障:确保屏障前的写操作先于屏障后的读操作std::atomicmemory_order_relaxed:不保证内存可见性和顺序性,只保证原子性memory_order_acquire: 本线程后续的读操作不会被重排到此操作之前memory_order_release: 本线程之前的写操作不会被重排到此操作之后memory_order_acq_rel: 本线程之前的写操作不会被重排到此操作之后,本线程后续的读操作不会被重排到此操作之前memory_order_seq_cst: 全局顺序,保证内存可见性和顺序性,但性能较低volatile≠ 原子操作 ≠ 内存屏障std::atomic+ 合适的内存序volatile在多线程中几乎从不用于同步目的lock-free)设计ringbuffer:环形缓冲区
数据结构:
head与tail)push操作,移动tail指针pop操作,移动head指针hybrid):操作:
push:进行写操作,需要进行判满pop:进行读操作,需要进行判空队列需要存储的数据类型:
int、char、float等struct、class(可能存在虚函数)等要避免伪共享
struct RingBuffer { std::atomic<int> producer_counter; // 生产者 std::atomic<int> consumer_counter; // 消费者 // 这两个变量可能在同一个64字节缓存行中 };一个生产者操作A,一个消费者操作B;二者没有数据竞争,但是:
MESI频繁同步,会导致性能急剧下降// 在C++当中,可以通过`alignas`来指定对齐方式 struct RingBuffer { alignas(64) std::atomic<int> producer_counter; // 生产者 alignas(64) std::atomic<int> consumer_counter; // 消费者 };优化:
Capacity的大小,优化成2的幂次方next = (curr + 1) % Capacity-------------->next = (curr + 1) & (Capacity - 1)接口设计:
push:入队POD),直接使用std::atomic即可非POD),可以考虑用到完美转发pop:出队size:获取队列大小// 入队 template<typename T> bool push(U&& value) { // 1.先判满 const std::size_t w = m_write.load(std::memory_order_relaxed); // SPSC, 考虑到性能最高,使用relaxed const std::size_t next_w = (w + 1) & (Capacity - 1); // 哨兵 if (next_w == m_read.load(std::memory_order_acquire)) return false; // acquire 保证读到最新的值 // 2.写入数据 new (&buffers[w]) T(std::forward<U>(value)); m_write.store(next_w, std::memory_order_release); // release 保证写入的值是最新的 return true; } bool pop(T& value) { // 1.先判空 const std::size_t r = m_read.load(std::memory_order_relaxed); if (r == m_write.load(std::memory_order_acquire)) return false; // 2.读取数据 value = std::move(*reinterpret_cast<T*>(&buffers[r])); // reinterpret_cast 对内存进行重新解释 reinterpret_cast<T *>(&buffers[r])->~T(); m_read.store((r + 1) & (Capacity - 1), std::memory_order_release); return true; } std::size_t size()const { const std::size_t r = m_read.load(std::memory_order_acquire); const std::size_t w = m_write.load(std::memory_order_acquire); return w >= r ? (w - r) : (Capacity - r + w); }struct VersionedValue { T value; std::atomic<uint64_t> version; };struct TaggedPointer { void* ptr; uint64_t tag; // 每次修改递增 };相对于无锁队列,设计就比较简单,基本上对于任何操作进行加锁,释放锁即可;但头疼点也随之而来,容易造成死锁。
有锁队列的选择多种多样,可以直接选择STL容器,如std::queue、std::deque、std::list等,进行动态扩容。
// 还是采用模板类方式构造 template<typename T, typename StorageType = std::deque<T>> class LockedQueue { private: std::mutex m_mutex; StorageType m_storage; public: LockedQueue() = default; ~LockedQueue() = default; void add(const T& item) { lock(); _queue.push_back(item); unlock(); } void lock() { this->_lock.lock(); } void unlock() { this->_lock.unlock(); } void pop_front() { std::lock_guard<std::mutex> lock(_lock); _queue.pop_front(); } };如何判断锁的持有时间?
malloc/free的开销)STL队列,比如queue<int> q,入队可能会涉及到内存分配、节点构造、指针修改等操作,这些操作很难进行原子化,一般都是加锁。STL-vector,在push_back时,如果空间不足,会进行扩容,扩容会触发重新分配内存;无法保证环形访问;而且删除效率低下。| 类型 | 时间复杂度 | 适用场景 | 性能 |
|---|---|---|---|
| SPSC | O(1) | 流水线处理 | 最高 |
| MPSC | O(n) | 日志收集 | 中等 |
| SPMC | O(n) | 事件广播 | 中等 |
| MPMC | O(n²) | 通用队列 | 最低 |
| 特性 | 无锁队列 | 有锁队列 |
|---|---|---|
| 开销 | 低(无线程上下文切换) | 高(有线程上下文切换) |
| 实现复杂度 | 高 | 低 |
| 内存开销 | 高 | 低 |
| 适用场景 | 高并发、低延迟 | 通用 |