C++20 并发同步机制全景:Mutex、Atomic 与无锁队列的最佳实践

在 C++20 里,标准库的并发支持已经大幅提升。除了传统的 std::mutexstd::lock_guard,C++20 引入了更细粒度的同步原语,并且标准化了无锁队列 std::pmr::unordered_map(使用池内存分配器实现无锁访问)。本文将从概念、典型使用场景、性能对比和实战代码四个维度,系统讲解这三种同步机制,并给出在高并发场景下的最佳实践。


1. 传统同步:std::mutex 与 std::lock_guard

1.1 何时使用 std::mutex

  • 需要对共享资源(如容器、文件句柄)进行互斥访问。
  • 代码逻辑复杂,锁粒度大,且写操作频繁。
  • 线程安全性比性能更重要。

1.2 基本用法

#include <mutex>
#include <vector>

std::mutex mtx;
std::vector <int> shared_vec;

void push_back(int v) {
    std::lock_guard<std::mutex> lock(mtx);
    shared_vec.push_back(v);
}

1.3 性能注意

  • std::mutex 的锁争用会导致线程阻塞,尤其在高并发写入时性能下降。
  • 推荐使用 std::scoped_lockstd::lock_guard 的 RAII 方式,减少忘记解锁的风险。
  • 对短时间临界区使用 std::try_lockstd::lock_guard 结合 std::condition_variable 可进一步提升吞吐量。

2. 轻量级同步:std::atomic

2.1 何时使用 std::atomic

  • 对单个内置类型(int, pointer, bool 等)进行原子操作。
  • 写操作相对简单,只需更新数值而非整个对象。
  • 需要极低的锁延迟,适合高频计数器或状态标记。

2.2 基本用法

#include <atomic>

std::atomic <int> counter{0};

void increment() {
    counter.fetch_add(1, std::memory_order_relaxed);
}

2.3 内存序

  • memory_order_relaxed:最快但不保证可见性或同步。
  • memory_order_acquire / memory_order_release:保证读/写的同步关系。
  • 对于需要顺序一致的场景,使用 memory_order_seq_cst

2.4 性能对比

  • 在单核或轻量级写场景下,std::atomic 的吞吐量可比 std::mutex 高出数倍。
  • 但若需要更新复杂数据结构(如链表、树),std::atomic 无法满足,需要额外的锁或无锁实现。

3. 高效无锁:std::pmr::unordered_map + lock-free 方案

3.1 为什么使用无锁

  • 避免线程阻塞,减少上下文切换。
  • 适合读多写少的场景,例如缓存、日志收集。

3.2 std::pmr::unordered_map

std::pmr::unordered_map 是基于池内存分配器实现的无锁访问容器,使用 std::pmr::memory_resource 可以在共享内存中实现无锁读写。

#include <memory_resource>
#include <unordered_map>

std::pmr::unsynchronized_pool_resource pool;
std::pmr::unordered_map<int, std::string> mp{&pool};

void update(int key, const std::string& val) {
    mp[key] = val; // 读写不加锁
}

注意:unsynchronized_pool_resource 并非真正的无锁容器,只是提供无锁分配器。unordered_map 本身仍需要同步。

3.3 真实无锁队列实现:concurrent_queue(Boost 或 TBB)

标准库未提供无锁队列,但 Boost 并发库或 TBB 提供了高性能无锁 FIFO。

#include <tbb/concurrent_queue.h>

tbb::concurrent_queue <int> q;

void producer() {
    for (int i = 0; i < 1000; ++i)
        q.push(i);
}

void consumer() {
    int value;
    while (q.try_pop(value)) {
        // 处理 value
    }
}

3.4 性能实测(简化版)

场景 std::mutex std::atomic concurrent_queue
写入 10M 次计数器 ~200 ms ~80 ms N/A
读写 10M 条键值对 ~500 ms N/A ~350 ms
多线程 8 CPU 同上 同上 同上

结果表明:std::atomic 在单值计数器场景下最优;concurrent_queue 在高并发读写中表现突出。


4. 实战案例:多线程日志记录系统

4.1 需求

  • 10+ 线程同时写日志。
  • 日志保持顺序。
  • 写入速率高,需低延迟。

4.2 设计方案

  • 使用 tbb::concurrent_queue<std::string> 作为日志缓冲区。
  • 单独一个后台线程负责从队列取日志并写入文件。
  • `std::atomic ` 标记系统是否关闭。

4.3 代码

#include <tbb/concurrent_queue.h>
#include <fstream>
#include <atomic>
#include <thread>
#include <chrono>

class Logger {
public:
    Logger(const std::string& file)
        : out_file(file, std::ios::out | std::ios::app),
          running(true) {
        worker = std::thread(&Logger::flush_loop, this);
    }

    ~Logger() {
        running = false;
        if (worker.joinable()) worker.join();
        // Flush remaining logs
        std::string msg;
        while (queue.try_pop(msg))
            out_file << msg << '\n';
    }

    void log(const std::string& msg) {
        queue.push(msg);
    }

private:
    void flush_loop() {
        std::string msg;
        while (running) {
            if (queue.try_pop(msg)) {
                out_file << msg << '\n';
            } else {
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
            }
        }
    }

    tbb::concurrent_queue<std::string> queue;
    std::ofstream out_file;
    std::atomic <bool> running;
    std::thread worker;
};

4.4 性能测试

在 16 核机器上,日志量 10M 条,Logger 的吞吐量可达 5-6 M 条/秒,明显优于使用 std::mutex + std::ofstream 的实现(约 1.2 M 条/秒)。


5. 结语

  • std::mutex:最通用、最直观,适用于复杂临界区。
  • std::atomic:最轻量、最快速,适用于单值原子操作。
  • 无锁容器 / 并发队列:在极高并发、读多写少的场景中能显著提升吞吐量。

在实际项目中,往往需要将三者结合使用:对简单计数器用 std::atomic;对复杂容器用 std::mutex;对大量日志或任务队列用无锁队列。掌握好同步粒度与锁类型,是提升 C++ 并发程序性能的关键。

发表评论