在并发编程中,我们常常遇到这样的场景:多个线程需要共享和修改同一个变量。假设我们有一个简单的布尔标志位stop,主线程将其设置为true时,工作线程应该停止运行。看起来很简单,对吧?
cpp复制bool stop = false;
void worker() {
while (!stop) {
// 执行任务
}
}
// 主线程
stop = true;
但实际情况可能让你大吃一惊——工作线程可能永远不会停止!这不是代码逻辑的问题,而是现代计算机体系结构带来的内存可见性问题。具体来说,有三个主要原因:
while(!stop)优化为while(true),因为它认为循环内没有修改stop的值提示:这个问题不仅存在于C++中,Java开发者可能熟悉类似的
volatile关键字,它解决了部分可见性问题,但不保证原子性。
std::atomic模板类为我们提供了三个关键保证,解决了上述问题:
原子性意味着操作是不可分割的。对于std::atomic<int> counter来说,counter++这样的操作会被编译为一条原子指令,而不是传统的"读取-修改-写入"三步操作。这避免了多线程环境下的竞态条件。
cpp复制std::atomic<int> counter(0);
// 线程安全的递增
counter++; // 等价于counter.fetch_add(1)
当一个线程修改了atomic变量的值,这个修改会立即对其他线程可见。这是通过内存屏障(Memory Barrier)实现的,确保修改被刷新到主内存,而不是停留在CPU缓存中。
默认情况下,std::atomic使用memory_order_seq_cst内存顺序,这提供了最强的顺序保证——所有线程看到的操作顺序是一致的。虽然这会带来一些性能开销,但对于大多数应用场景来说,这是最安全的选择。
让我们比较三种实现计数器的方式:
错误版本(非线程安全):
cpp复制int counter = 0; // 非原子变量
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 非原子操作
}
}
// 多线程调用increment()会导致计数不准确
互斥锁版本:
cpp复制std::mutex mtx;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}
}
// 线程安全但性能较差
原子版本:
cpp复制std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 原子操作
}
}
// 线程安全且高效
性能对比(4线程,各递增100,000次):
| 实现方式 | 执行时间(ms) | 正确性 |
|---|---|---|
| 非原子 | ~5 | ❌ |
| 互斥锁 | ~50 | ✅ |
| 原子操作 | ~10 | ✅ |
这是atomic最典型的应用场景之一:
cpp复制std::atomic<bool> running(true);
void worker() {
while (running.load()) { // 安全读取
// 执行任务
}
}
// 主线程安全地停止工作线程
running.store(false);
虽然大多数开发者不需要自己实现无锁数据结构,但了解其基础很有价值。std::atomic提供了compare_exchange_strong和compare_exchange_weak操作,这是实现无锁算法的关键:
cpp复制std::atomic<int> value(0);
void try_increment() {
int expected = value.load();
while (!value.compare_exchange_weak(expected, expected + 1)) {
// 如果value不等于expected,expected会被更新为当前value
// 然后重试
}
}
理解何时使用atomic,何时使用mutex至关重要:
| 特性 | std::atomic | std::mutex |
|---|---|---|
| 适用场景 | 简单变量(bool, int等) | 复杂数据结构或代码块 |
| 阻塞行为 | 非阻塞 | 可能阻塞 |
| 性能 | 更高 | 较低 |
| 保护范围 | 单个变量 | 任意代码区域 |
| 组合操作原子性 | 有限支持(需CAS操作) | 天然支持 |
经验法则:当只需要保护单个基本类型变量时,优先考虑atomic;当需要保护复杂数据结构或确保多个操作的原子性时,使用mutex。
一个常见的误解是认为atomic变量可以保证多个操作的原子性。例如:
cpp复制std::atomic<int> x(10);
// 不安全!判断和修改不是原子操作
if (x > 0) {
x--;
}
正确的做法是使用CAS(Compare-And-Swap)操作:
cpp复制int expected = x.load();
while (expected > 0 && !x.compare_exchange_weak(expected, expected - 1)) {
// 重试直到成功
}
虽然atomic性能优于mutex,但不应该滥用:
std::atomic允许指定内存顺序,除非你非常了解内存模型,否则应该使用默认的memory_order_seq_cst:
cpp复制// 默认,最安全但性能略低
counter.store(42, std::memory_order_seq_cst);
// 更宽松的内存顺序(性能更高但更危险)
counter.store(42, std::memory_order_release);
对于熟悉Java的开发者,这里有一个快速对照表:
| Java | C++ | 说明 |
|---|---|---|
| volatile | std::atomic | 但Java的volatile不保证原子性 |
| AtomicBoolean | std::atomic |
功能相似 |
| AtomicInteger | std::atomic |
功能相似 |
| get() | load() | 读取操作 |
| set() | store() | 写入操作 |
| compareAndSet() | compare_exchange_strong() | CAS操作 |
关键区别:
虽然atomic比mutex更高效,但仍然有一些性能开销。以下是一些优化建议:
memory_order_relaxed例如,对于频繁更新的计数器,可以考虑线程本地存储+定期合并:
cpp复制thread_local int local_counter = 0;
std::atomic<int> global_counter(0);
void increment() {
local_counter++;
if (local_counter % 100 == 0) { // 定期同步
global_counter.fetch_add(local_counter);
local_counter = 0;
}
}
cpp复制std::atomic<int> item_count(0);
std::mutex queue_mutex;
std::queue<Item> queue;
std::condition_variable cv;
void producer() {
while (true) {
Item item = produce_item();
{
std::lock_guard<std::mutex> lock(queue_mutex);
queue.push(item);
}
item_count.fetch_add(1); // 原子递增
cv.notify_one();
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(queue_mutex);
cv.wait(lock, []{ return item_count.load() > 0; });
Item item = queue.front();
queue.pop();
item_count.fetch_sub(1); // 原子递减
lock.unlock();
process_item(item);
}
}
虽然C++11后更推荐使用call_once,但了解这种模式仍有价值:
cpp复制class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::mutex mtx;
Singleton() {}
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load();
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load();
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp);
}
}
return tmp;
}
};
多线程问题往往难以复现和调试,以下是一些实用技巧:
-fsanitize=thread编译选项cpp复制std::atomic<int> counter(0);
std::vector<std::string> log;
void worker() {
for (int i = 0; i < 100; ++i) {
int old = counter.fetch_add(1);
std::string msg = "Thread " + std::to_string(std::this_thread::get_id()) +
" incremented to " + std::to_string(old + 1);
// 注意:日志记录本身需要线程安全
std::lock_guard<std::mutex> lock(log_mutex);
log.push_back(msg);
}
}
C++20对atomic有一些增强:
cpp复制std::atomic_flag flag;
flag.wait(false); // 等待flag变为true
flag.notify_one(); // 唤醒等待的线程
cpp复制std::atomic<double> atomic_double(0.0);
atomic_double.fetch_add(1.5);
std::atomic<std::shared_ptr<T>>在实际工程中,atomic很少单独使用,而是与其他并发工具配合:
cpp复制std::atomic<bool> data_ready(false);
std::mutex mtx;
std::condition_variable cv;
Data data;
void producer() {
data = prepare_data();
data_ready.store(true);
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return data_ready.load(); });
process_data(data);
}
在构建并发系统时,建议采用分层设计:
这种分层方法使系统更易于理解、维护和调试。