在并发编程中,计数器往往是最常见的共享数据结构之一。无论是实现生产者-消费者模型、统计任务完成数量,还是用于实现自增 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/release或memory_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
}
同样的代码可以替换为 MutexCounter 或 ReadWriteCounter,观察性能差异。
7. 常见错误与排查
- 忘记使用原子或锁:导致数据竞争、未定义行为。
- 使用
memory_order_relaxed但需要可见性:若计数结果需要与其它状态同步,需改用memory_order_acquire/release。 - 互斥锁死锁:确保锁的获取顺序一致,避免嵌套锁导致死锁。
- 对
std::atomic的误用:例如int x; atomic_fetch_add(&x, 1)会导致 UB,必须使用 `std::atomic `。
8. 小结
- 最简单:`std::atomic `,几乎无锁,性能最高。
- 最灵活:
std::mutex或std::shared_mutex,适合需要与其它共享状态同步的情况。 - 混合模式:在需要计数器与其它状态一起维护时,结合使用原子和共享锁最优。
根据具体应用场景选择合适的实现,既能保证线程安全,也能满足性能需求。