C++并发编程:条件变量与原子操作

本文是 C++ 系列教程的第 24 篇。上一篇讲解了线程与互斥锁,本篇深入同步机制进阶:条件变量与生产者消费者模型、原子类型与内存序、无锁编程入门与读写锁,覆盖 9 个完整示例代码。

一、条件变量(std::condition_variable)

1.1 为什么需要条件变量

互斥锁只能保证「同时只有一个人进房间」,但无法解决「等待某个条件成立」的问题。条件变量允许线程阻塞等待 某个条件,直到另一线程通知它醒来,避免忙等待浪费 CPU:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;

mutex mtx;
condition_variable cv;
bool ready = false;

void waiter() {
    unique_lock<mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });   // 阻塞直到 ready == true
    cout << "等待者被唤醒,开始工作" << endl;
}

void notifier() {
    this_thread::sleep_for(chrono::milliseconds(500));
    {
        lock_guard<mutex> lock(mtx);
        ready = true;                     // 修改条件必须在锁内
    }
    cv.notify_one();                      // 唤醒一个等待线程
}

int main() {
    thread t1(waiter);
    thread t2(notifier);
    t1.join();
    t2.join();
    return 0;
}

wait(lock, predicate) 的谓词重载等价于 while (!pred()) wait(lock),能自动处理虚假唤醒(spurious wakeup)

1.2 生产消费者模型

条件变量最经典的应用场景。生产者往队列放数据并通知,消费者阻塞等待并从队列取数据:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
using namespace std;

mutex mtx;
condition_variable cv;
queue<int> tasks;
bool done = 
false;

void producer() {
    for (int i = 1; i <= 5; ++i) {
        {
            lock_guard<mutex> lock(mtx);
            tasks.push(i);
            cout << "生产: " << i << endl;
        }
        cv.notify_one();                  // 通知消费者
        this_thread::sleep_for(chrono::milliseconds(100));
    }
    {
        lock_guard<mutex> lock(mtx);
        done = true;
    }
    cv.notify_all();                      // 唤醒所有消费者(处理结束)
}

void consumer(int id) {
    while (true) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, []{ return !tasks.empty() || done; });
        if (!tasks.empty()) {
            int task = tasks.front();
            tasks.pop();
            cout << "  消费者 " << id << " 消费: " << task << endl;
        } else if (done) {
            break;                        // 生产结束且队列为空
        }
    }
}

int main() {
    thread p(producer);
    thread c1(consumer, 1);
    thread c2(consumer, 2);
    p.join();
    c1.join();
    c2.join();
    cout << "生产消费完成" << endl;
    return 0;
}

关键点notify_one 唤醒单个线程,notify_all 唤醒全部;等待条件必须用 while/谓词重载以防虚假唤醒;done 标志防止消费者永久阻塞。

二、原子操作(std::atomic)

2.1 原子类型基础

std::atomic<T> 提供无锁(或锁内部实现的)原子操作,fetch_addexchangecompare_exchange 等保证读-改-写完整性,无需互斥锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
using namespace std;

atomic<int> counter{0};

void increment() {
    for (int i = 0; i < 100000; ++i) counter.fetch_add(1);
}

int main() {
    vector<thread> threads;
    for (int i = 0; i < 
4; ++i) threads.emplace_back(increment);
    for (auto& t : threads) t.join();

    cout << "counter = " << counter.load() << endl;   // 400000,无需加锁
    return 0;
}

fetch_add 原子完成「读-加-写」。load() 原子读取,store() 原子写入。

2.2 compare_exchange 与自旋锁

compare_exchange_strong 是 CAS 指令的封装:当前值等于期望值时写入新值,否则更新期望值为实际值。可用它实现自旋锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
using namespace std;

class SpinLock {
    atomic<bool> flag{false};
public:
    void lock() {
        // 期望 false,尝试写入 true;失败则自旋重试
        while (flag.exchange(true)) {
            // 空转等待(可加 this_thread::yield() 让出 CPU)
        }
    }
    void unlock() {
        flag.store(false);
    }
};

SpinLock spin;
int counter = 0;

void work() {
    for (int i = 0; i < 50000; ++i) {
        lock_guard<SpinLock> lock(spin);
        counter++;
    }
}

int main() {
    vector<thread> threads;
    for (int i = 0; i < 4; ++i) threads.emplace_back(work);
    for (auto& t : threads) t.join();
    cout << "counter = " << counter << endl;   // 200000
    return 0;
}

自旋锁适合临界区极短 的场景;临界区长时应使用会阻塞的 std::mutex,避免浪费 CPU。

三、内存序(Memory Order)

3.1 为什么要关心内存序

编译器与 CPU 可能重排指令(单线程不可见,多线程可导致意外行为)。内存序控制重排边界:

cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<bool> ready{false};
int data = 0;

void producer() {
    data = 42;                    // 写数据
    ready.store(tru
e, memory_order_release);   // 释放语义:之前的写操作全部可见
}

void consumer() {
    while (!ready.load(memory_order_acquire)) { }  // 获取语义:确保读到最新 data
    cout << "data = " << data << endl;             // 保证读到 42
}

int main() {
    thread t1(producer);
    thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

release(写侧)与 acquire(读侧)配对使用,形成同步关系(happens-before),保证生产者写入的数据对消费者可见。

3.2 常见内存序对比

内存序 语义 用途
memory_order_relaxed 无同步,仅保证原子性 计数器、统计量
memory_order_acquire 其后读写不可越过本操作 读取标志位
memory_order_release 其前读写不可越过本操作 发布数据
memory_order_acq_rel acquire + release RMW 操作
memory_order_seq_cst 全序一致(默认) 复杂同步,易推理
cpp 复制代码
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;

atomic<long long> hits{0};

void report() {
    for (int i = 0; i < 1000000; ++i) {
        hits.fetch_add(1, memory_order_relaxed);   // 只需原子性,无需同步

    }
}

int main() {
    thread t1(report);
    thread t2(report);
    t1.join();
    t2.join();
    cout << "hits = " << hits.load(memory_order_relaxed) << endl;  // 2000000
    return 0;
}

经验:默讙 seq_cst` 最容易正确,性能足够时优先使用;只有基准测试证明是瓶颈,才降级为 relaxed/acquire/release 并仔细论证正确性。

四、实战:线程安全的任务队列

综合运用互斥锁、条件变量与 RAII 封装一个可直接复用的线程安全队列:

cpp 复制代码
#include <iostream>
#inc
lude <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <optional>
using namespace std;

template <typename T>
class ThreadSafeQueue {
    mutable mutex mtx;
    condition_variable cv;
    queue<T> q;
public:
    void push(T value) {
        {
            lock_guard<mutex> lock(mtx);
            q.push(move(value));
        }
        cv.notify_one();
    }

    // 阻塞弹出
    T pop() {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [this]{ return !q.empty(); });
        T value = move(q.front());
        q.pop();
        return value;
    }

    // 非阻塞尝试弹出
    optional<T> tryPop() {
        lock_guard<mutex> lock(mtx);
        if (q.empty()) return nullopt;
        T value = move(q.front());
        q.pop();
        return value;
    }

    size_t size() const {
        lock_guard<mutex> lock(mtx);
        return q.size();
    }
};

int main() {
    ThreadSafeQueue<int> tq;

    thread producer([&] {
        for (int i = 1; i <= 6; ++i) {
            tq.push(i);
            this_thread::sleep_for(chrono::milliseconds(50));
        }
    });

    thread consumer([&] {
        for (int i = 0; i < 6; ++i) {
            int v = tq.pop();      // 阻塞等待
            cout << "取出: " << v << "(队列剩余 " << tq.size() << ")" << endl;
        }
    });

    producer.join();
    consumer.join();
    cout << "线程安全队列测试完成" << endl;
    return 0;
}

该队列把锁与条件变量的复杂性封装在内部,对外提供 push/pop/tryPop/size 四个安全接口,是生产环境常用的基础组件。

总结

本篇讲解了并发同步进阶技术:condition_variable 实现阻塞等待与通知(含谓词重载防虚假唤醒)、生产者消费者模型 的完

整实现、atomic 原子类型 (fetch_add/CAS 自旋锁)避免数据竞争、内存序(relaxed/acquire/release/seq_cst)控制可见性与重排,最后封装了一个线程安全的任务队列组件。建议配合互斥锁按场景选用:临界区短用原子,等待条件用条件变量,常规保护用 mutex。

下一篇将讲解 C++ 并发编程:异步任务与线程池实战(std::async、future、packaged_task 与线程池实现),敬请期待!

相关推荐
ttwuai7 分钟前
Go 后台附件迁到对象存储后,path、cdnUrl 和 tenant_id 怎么一起验?
开发语言·后端·golang
一只小阿乐11 分钟前
java 语法学习 1
java·开发语言·学习
dear_bi_MyOnly11 分钟前
函数模块化:企业级项目高效之道
c++·后端·学习
码匠许师傅16 分钟前
【设计模式精讲】13.装饰器模式(Decorator)
c++·设计模式·uml·装饰器模式
卢锡荣18 分钟前
Type-C口电子产品离不开的C口逻辑专用控制芯片介绍
c语言·开发语言
2601_9620739718 分钟前
苍穹外卖-day07(Spring Cache & 购物车业务逻辑)
java·后端·spring
顶点多余21 分钟前
算法哪些事儿---2
java·开发语言
方知我22 分钟前
NumPy一小时速成
开发语言·python·numpy
深入云栈1 小时前
CompleteFuture VS CompletableFuture:Netty 为何自研 Future
java·架构
OPEN-F1 小时前
C++综合实战:面向对象图书管理系统
开发语言·c++