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_add、exchange、compare_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 与线程池实现),敬请期待!

相关推荐
1024奇点7 小时前
【仓颉语言入门 · 第二篇 第 22 课】
开发语言·ide·开源
Warren2Lynch8 小时前
C4 模型详解:利用 Visual Paradigm 可视化软件架构的综合指南
开发语言·uml
天河归来8 小时前
第三方接口不稳定,如何让核心任务继续完成
java·大数据
FlightYe8 小时前
视界原理之3D视频(四):裸眼、眼镜和头显
android·linux·c++·3d·ffmpeg·音视频
Knight_AL8 小时前
Java 文件下载时文件名中的 `#` 被转换成 `%23`?一次 URL 编码与解码问题排查
java·开发语言
FRing039 小时前
vue2干货总结2(v-bind、v-for、v-model、指令修饰符、v-bind操作class和style)
开发语言·前端·vue.js·vue2
by209999 小时前
C++ 智能指针的所有权与使用
开发语言·c++·经验分享
砚底藏山河9 小时前
python量化自定义周期 K 线合成
java·开发语言·数据库·python·金融
IT召唤狮10 小时前
开源企业级零信任项目Pangolin,零信任能挡住PHP核弹漏洞吗?
开发语言·开源·php
知已难寻10 小时前
03 · OpenStack的计算管理 Nova
运维·服务器·开发语言·云原生·云计算·openstack·开源软件