问:C++中如何安全地实现多线程计数器?

在并发编程中,计数器往往是最常见的共享数据结构之一。无论是实现生产者-消费者模型、统计任务完成数量,还是用于实现自增 ID,计数器都需要保证多线程访问时的原子性与可见性。下面以 C++17 标准为例,介绍几种常用且安全的实现方式,并讨论它们的优缺点。

1. 使用 std::atomic

#include <atomic>
#include <thread>
#include <vector>
#include <iostream>

class AtomicCounter {
public:
    AtomicCounter() : value_(0) {}

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

    int get() const { return value_.load(std::memory_order_relaxed); }

private:
    std::atomic <int> value_;
};

关键点

  • `std::atomic ` 提供了原子增减操作,内部使用 CPU 的原子指令实现,性能非常高。
  • memory_order_relaxed 在只需要计数器本身原子性,不关心与其它共享变量的同步时使用。若需要与其它共享状态同步,可改用 memory_order_acquire/releasememory_order_seq_cst

适用场景

  • 计数器是独立于其它共享状态的。
  • 需要极低延迟的并发计数(例如高频统计)。

2. 使用互斥锁(std::mutex)

#include <mutex>

class MutexCounter {
public:
    MutexCounter() : value_(0) {}

    void increment() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++value_;
    }

    int get() const {
        std::lock_guard<std::mutex> lock(mutex_);
        return value_;
    }

private:
    mutable std::mutex mutex_;
    int value_;
};

关键点

  • std::lock_guard 保证在作用域结束时自动释放锁,避免死锁。
  • 需要 mutable 以便在 get() 成员函数(const)中也能锁定。

适用场景

  • 计数器与其它共享资源需要一起同步(例如需要对多个变量同时加锁)。
  • 对于低并发或对性能要求不高的场景。

3. 使用原子+读写锁(std::shared_mutex)

如果计数器读取频繁,而写入相对少,可以采用共享锁:

#include <shared_mutex>

class ReadWriteCounter {
public:
    void increment() {
        std::unique_lock<std::shared_mutex> lock(mutex_);
        ++value_;
    }

    int get() const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        return value_;
    }

private:
    mutable std::shared_mutex mutex_;
    int value_;
};
  • unique_lock 用于写操作,shared_lock 用于读操作。
  • 读操作可以并发进行,写操作仍然是互斥的。

适用场景

  • 计数器读多写少,例如日志计数、请求计数等。

4. 结合 std::atomic 与读写锁的混合模式

在某些复杂系统中,计数器可能与其它共享状态一起维护。可以使用 std::atomic 对计数器操作,同时使用 std::shared_mutex 对其它状态加锁。这样既保持计数器高效,又保证了整体一致性。

struct SharedState {
    std::atomic <int> counter;
    std::shared_mutex lock;
    // 其它共享成员...
};

5. 性能比较

实现方式 写操作性能 读操作性能 适用场景
std::atomic 极快 极快 单独计数,毫秒级延迟需求
std::mutex 较慢 较慢 与其它资源同步
std::shared_mutex 中等 读多写少的计数
atomic+shared_mutex 变动 变动 计数+其它状态同步

6. 代码演示

int main() {
    AtomicCounter ac;
    std::vector<std::thread> threads;
    for (int i = 0; i < 8; ++i) {
        threads.emplace_back([&ac]() {
            for (int j = 0; j < 100000; ++j) {
                ac.increment();
            }
        });
    }
    for (auto& t : threads) t.join();
    std::cout << "AtomicCounter result: " << ac.get() << std::endl; // 800000
}

同样的代码可以替换为 MutexCounterReadWriteCounter,观察性能差异。

7. 常见错误与排查

  1. 忘记使用原子或锁:导致数据竞争、未定义行为。
  2. 使用 memory_order_relaxed 但需要可见性:若计数结果需要与其它状态同步,需改用 memory_order_acquire/release
  3. 互斥锁死锁:确保锁的获取顺序一致,避免嵌套锁导致死锁。
  4. std::atomic 的误用:例如 int x; atomic_fetch_add(&x, 1) 会导致 UB,必须使用 `std::atomic `。

8. 小结

  • 最简单:`std::atomic `,几乎无锁,性能最高。
  • 最灵活std::mutexstd::shared_mutex,适合需要与其它共享状态同步的情况。
  • 混合模式:在需要计数器与其它状态一起维护时,结合使用原子和共享锁最优。

根据具体应用场景选择合适的实现,既能保证线程安全,也能满足性能需求。

发表评论