在 C++20 中,标准库对并发编程提供了更丰富、更直观的工具,特别是 std::atomic_ref 的加入,让我们能够在不改动现有数据结构的前提下,实现原子操作。本文将通过一个实际场景:多线程计数器池,演示如何结合 std::atomic_ref、std::shared_mutex 和 std::span 来构建高性能、低锁竞争的并发容器。
一、背景与需求
假设我们需要维护一个整数计数器集合,每个计数器对应不同业务场景。要求:
- 高并发写:大量线程频繁自增或自减计数器。
- 低延迟:读取时应尽量避免锁,或者锁的粒度尽可能细。
- 可扩展性:后续可能需要动态插入或删除计数器。
传统做法是使用 `std::atomic
` 作为计数器单元,配合 `std::unordered_map>` 或者 `std::map`。但这样会导致: – 每个计数器都必须是 `std::atomic `,内存占用较大。 – 若使用指针或引用,需要对 `std::atomic ` 进行额外包装。 – 动态添加计数器时,需要重新分配整个容器。 C++20 的 `std::atomic_ref` 解决了上述问题:它是一个轻量级的引用包装器,允许我们对已有的非原子对象执行原子操作,而无需改动对象本身。结合 `std::shared_mutex`(读写锁)和 `std::span`(视图),可以实现既灵活又高效的并发容器。 ## 二、核心技术点 ### 1. std::atomic_ref “`cpp #include #include int main() { int x = 10; std::atomic_ref ax{x}; ax.fetch_add(5); // 原子地自增 5 std::cout #include #include #include #include #include #include #include #include class CounterPool { public: // 获取或创建计数器 void increment(const std::string& name, int delta = 1) { std::unique_lock lk(mutex_); auto it = counters_.find(name); if (it == counters_.end()) { // 动态插入新计数器,初始化为 0 counters_.emplace(name, 0); it = counters_.find(name); } std::atomic_ref atomicVal(it->second); atomicVal.fetch_add(delta, std::memory_order_relaxed); } // 读取计数器值 std::optional get(const std::string& name) const { std::shared_lock lk(mutex_); auto it = counters_.find(name); if (it == counters_.end()) return std::nullopt; std::atomic_ref atomicVal(it->second); return atomicVal.load(std::memory_order_relaxed); } // 批量读取:返回一个 std::span(只读视图) std::vector snapshot() const { std::shared_lock lk(mutex_); std::vector result; result.reserve(counters_.size()); for (auto& [name, val] : counters_) { std::atomic_ref atomicVal(val); result.push_back(atomicVal.load(std::memory_order_relaxed)); } return result; // 这里返回拷贝,若想保持视图可用需改用 span 对外 } private: mutable std::shared_mutex mutex_; std::unordered_map counters_; }; “` ### 说明 1. **计数器类型**:`unordered_map`。计数器本身不是原子类型,但我们通过 `std::atomic_ref` 在需要时原子地操作。 2. **锁粒度**:所有增删操作都需要持有写锁,读取使用共享锁。由于计数器修改是原子操作,写锁的持有时间极短,几乎不成为瓶颈。 3. **动态扩容**:`increment` 在计数器不存在时自动插入,后续无需重新组织容器。 ### 使用示例 “`cpp #include “counter_pool.hpp” #include #include #include int main() { CounterPool pool; const int threadCnt = 8; const int opsPerThread = 100000; // 多线程自增 std::vector threads; for (int i = 0; i ` | `unordered_map>` | 写锁 | 1.2M | | `std::atomic_ref ` + 写锁 | 同上 | 写锁 | 1.5M | | `std::atomic_ref ` + `shared_mutex` + `shared_lock` | `unordered_map` | 写锁 + 共享锁 | 1.7M | 从表格可见,使用 `std::atomic_ref` 并结合 `shared_mutex` 能够降低锁争用,提升吞吐量。 ## 五、总结 C++20 为并发编程提供了更细粒度、更灵活的工具。通过 `std::atomic_ref`,我们可以在不改变数据结构的情况下,为普通对象提供原子操作;结合 `std::shared_mutex`,实现读多写少的场景下高效的并发容器。实际开发中,建议先评估业务需求,如果大部分操作为读,使用 `shared_mutex`;如果读写比例不高,直接使用 `std::atomic` 也可;而 `std::atomic_ref` 则是处理已有代码库时的天然利器。 在未来的 C++23 及更高版本中,可能会出现更专门的并发容器(如 `std::unordered_map>` 的轻量化版本),但目前使用 `std::atomic_ref` 与标准锁的组合,已能满足大多数高性能并发计数需求。