c++并发--同步

1.std::condition_variable(条件变量)

1.1.核心作用

条件变量用于线程间等待某个条件成立,配合 std::mutex 使用,解决"忙等"问题。

1.2.为什么需要它?

不用条件变量的经典错误写法:

c 复制代码
// 错误:忙等待,浪费 CPU
while (!ready) { /* 空转 */ }

// 错误:sleep 轮询,响应慢且仍浪费 CPU
while (!ready) { std::this_thread::sleep_for(10ms); }

1.3.三个关键操作

c 复制代码
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// 等待方
void waiter() {
    std::unique_lock<std::mutex> lock(mtx);
    // wait 会做两件事:
    // 1. 原子地释放锁并阻塞(防止错过 notify)
    // 2. 被唤醒后重新获取锁,并检查谓词
    cv.wait(lock, [] { return ready; });  // 带谓词的版本(推荐)
    // 等价于:
    // while (!ready) cv.wait(lock);
    std::cout << "条件满足,继续执行\n";
}

// 通知方
void notifier() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;  // 必须先改状态再通知
    }  // 先解锁再 notify,减少被唤醒线程等待锁的时间
    cv.notify_one();   // 唤醒一个等待线程
    // cv.notify_all(); // 唤醒所有等待线程
}

为什么必须配合 unique_lock

  • wait 需要在阻塞时释放锁,被唤醒时重新加锁------锁的所有权需要可转移,unique_lock 满足(lock_guard 不行)。
  • 这也是 wait 接收 unique_lock& 而非 mutex& 的原因。

经典坑点:

  • 虚假唤醒(spurious wakeup):wait 可能在没有 notify 的情况下返回,所以必须用 while 循环或带谓词版本检查条件。
  • 先改状态再通知:否则可能通知后、等待方还没进入 wait,导致永久阻塞(进入wait后,不再发通知了)。
  • wait 返回时不保证条件仍成立:被唤醒到重新拿到锁之间,其他线程可能又改了状态。不过拿到锁后,会进行谓词检测,可以保证此时谓词检测不通过,释放锁,再次等待。

典型应用:生产者-消费者队列

c 复制代码
template <typename T>
class BlockingQueue {
    std::mutex mtx;
    std::condition_variable cv_not_empty, cv_not_full;
    std::queue<T> q;
    size_t capacity;
public:
    void push(T val) {
        std::unique_lock<std::mutex> lock(mtx);
        cv_not_full.wait(lock, [&] { return q.size() < capacity; });
        q.push(std::move(val));
        cv_not_empty.notify_one();
    }
    T pop() {
        std::unique_lock<std::mutex> lock(mtx);
        cv_not_empty.wait(lock, [&] { return !q.empty(); });
        T val = std::move(q.front());
        q.pop();
        cv_not_full.notify_one();
        return val;
    }
};

1.3.1.条件等待细节

带谓词的 cv.wait(lock, pred) 的标准语义等价于:

c 复制代码
// 标准库大致实现
template <typename Lock, typename Pred>
void wait(Lock& lock, Pred pred) {
    while (!pred()) {   // 注意:pred 的调用发生在持有锁期间
        wait(lock);     // 释放锁 + 阻塞(原子操作),被唤醒后重新加锁
    }
}

完整流程拆解:

  • 进入时(还没睡眠过):
c 复制代码
加锁 → 检查 pred()
        ├─ true  → 直接通过,不睡眠
        └─ false → 释放锁(原子地进入阻塞)
  • 被唤醒后:
c 复制代码
从阻塞返回 → 重新获得锁 → 检查 pred()
                              ├─ true  → 返回,继续执行业务逻辑
                              └─ false → 再次释放锁,重新进入阻塞

2.std::condition_variable_any

condition_variable 的区别

std::condition_variable 只能与 std::mutex 配合使用,而 condition_variable_any 可以与任何满足基本锁要求的互斥类型一起工作(BasicLockable:只需有 lock()unlock())。

c 复制代码
std::condition_variable_any cv_any;
std::shared_mutex smtx;  // shared_mutex 不是普通 mutex!

void reader() {
    std::shared_lock lock(smtx);  // 读锁
    cv_any.wait(lock, [] { return data_ready; });
}
void writer() {
    {
        std::unique_lock lock(smtx);
        data_ready = true;
    }
    cv_any.notify_all();
}

代价:

condition_variable_any 是通用实现,性能略低于专门优化的 std::condition_variable。除非确实需要非标准互斥类型(如 shared_mutex、用户自定义锁、甚至跨进程的锁),否则优先用 condition_variable

3.std::promise(承诺)

3.1.核心模型

promise 是一次性单写者通道:某线程通过 set_value / set_exception 写入结果,结果自动存到关联的 shared state(共享状态)中,供未来的 future 读取。

c 复制代码
std::promise<int> p;
std::future<int> f = p.get_future();  // 必须在 set_value 之前获取

std::thread t([&p] {
    std::this_thread::sleep_for(1s);
    p.set_value(42);  // 写入结果,此后 future 可读到
});
std::cout << f.get();  // 阻塞直到结果就绪
t.join();

set_value 之后:

set_value 只能调用一次(第二次调用抛 std::future_error)。它是线程安全的------可与 future::get 并发执行。

异常传递:

c 复制代码
std::thread t([&p] {
    try {
        throw std::runtime_error("出错了");
    } catch (...) {
        p.set_exception(std::current_exception());  // 把异常存进共享状态
    }
});
try {
    f.get();  // 会重新抛出该异常
} catch (const std::exception& e) {
    std::cout << e.what();
}

使用场景:

当你自己创建线程并希望它向调用方返回结果/异常时,promise 是最底层的原语(asyncpackaged_task 都是基于它构建的)。

4.std::future(期物)

核心模型:

future 是共享状态的读端句柄,代表一个"未来才会有的值"。三种就绪方式:

  • promise::set_value 赋值
  • packaged_task 执行完毕
  • async 任务完成

关键成员函数:

c 复制代码
std::future<int> f = ...;

int v = f.get();          // 阻塞直到就绪;只能调用一次;移动语义取出结果
f.wait();                 // 仅阻塞等待,不取结果
f.wait_for(100ms);        // 限时等待,返回 future_status
f.wait_until(tp);

f.valid();                // 是否关联了共享状态
f.share();                // 转为 shared_future(调用后本对象失效)

future_status

c 复制代码
auto status = f.wait_for(500ms);
switch (status) {
    case std::future_status::ready:     /* 就绪 */ break;
    case std::future_status::timeout:   /* 超时 */ break;
    case std::future_status::deferred:  /* 任务延迟未启动(launch::deferred)*/ break;
}

关键限制:

future 是一次性、独占的:get() 只能调用一次,第二次会抛 future_error(因为结果是被 move 出来的)。这也是 shared_future 存在的理由。

析构行为(重要陷阱):

  • 如果 future 关联的是 std::async 启动的任务且未 get/waitfuture 析构时会阻塞直到任务完成。
  • 如果关联的是 deferred 任务,析构不会执行该任务(任务被丢弃)。

5.std::shared_future

future 的区别:

shared_future 允许多个线程多次读取同一个结果。

c 复制代码
std::promise<int> p;
std::shared_future<int> sf = p.get_future().share();  // 或 p.get_future() 隐式转换

// 多个线程可同时 get()
std::thread t1([sf] { std::cout << sf.get(); });
std::thread t2([sf] { std::cout << sf.get(); });  // OK,可复制
特性 future shared_future
复制 不可复制,只可移动 可复制
get() 次数 仅一次 任意多次,并发安全
典型用途 一对一传递结果 广播结果给多个等待者

使用场景:

一次计算、多处消费,例如:

c 复制代码
// 主线程加载配置,多个工作线程等待配置就绪
std::shared_future<Config> config_ready = std::async(std::launch::async, load_config).share();
std::thread workers[4];
for (auto& w : workers)
    w = std::thread([config_ready] { process(config_ready.get()); });

注意:shared_future 本身对象不是线程安全的(复制/析构需外部同步),但并发调用 get() 是安全的。

6.std::packaged_task

核心模型

把可调用对象 + 结果通道打包在一起:包装后的函数对象被调用时,返回值(或异常)自动存入关联的 shared state。

c 复制代码
std::packaged_task<int(int, int)> task([](int a, int b) { return a + b; });
std::future<int> f = task.get_future();

task(3, 4);              // 在任意线程调用(不一定在创建它的线程)
std::cout << f.get();    // 7

关键用途:线程池

packaged_task 是构建任务队列/线程池的标准方式------调用方不关心任务在哪个线程执行,只通过 future 取结果:

c 复制代码
class ThreadPool {
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex mtx; std::condition_variable cv;
    bool stop = false;
public:
    ThreadPool(size_t n) {
        for (size_t i = 0; i < n; ++i)
            workers.emplace_back([this] {
                while (true) {
                    std::function<void()> task;
                    { std::unique_lock lk(mtx);
                      cv.wait(lk, [&] { return stop || !tasks.empty(); });
                      if (stop && tasks.empty()) return;
                      task = std::move(tasks.front()); tasks.pop(); }
                    task();
                }
            });
    }
    template <typename F, typename... Args>
    auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>> {
        using R = std::invoke_result_t<F, Args...>;
        auto task = std::make_shared<std::packaged_task<R()>>(
            std::bind(std::forward<F>(f), std::forward<Args>(args)...));
        std::future<R> fut = task->get_future();
        { std::lock_guard lk(mtx); tasks.emplace([task] { (*task)(); }); }
        cv.notify_one();
        return fut;
    }
    ~ThreadPool() {
        { std::lock_guard lk(mtx); stop = true; }
        cv.notify_all();
        for (auto& w : workers) w.join();
    }
};

// 使用
ThreadPool pool(4);
auto fut = pool.submit([](int x) { return x * x; }, 5);
std::cout << fut.get();  // 25

6.1.逐行拆解 submit 的实现

6.1.1.模板签名部分

  • 万能引用(forwarding reference
c 复制代码
template <typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>

F&& fF 是推导出来的模板参数,所以 F&& 是万能引用而非右值引用:

调用方式 F 推导为 f 的类型
submit([](int x){...}, 5) lambda 类型(值) lambda 的左值引用
submit(std::move(func_obj), 5) 同类型 右值引用
submit(some_func, 5) 函数指针类型 指针的左值引用

配合后面的 std::forward 实现完美转发:左值保持左值、右值保持右值,lambda 既可以拷贝也可以移动进来。

  • std::invoke_result_t<F, Args...> ------ 推导返回值类型
c 复制代码
auto submit(...) -> std::future<std::invoke_result_t<F, Args...>>
//                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                              "用 F 调用 Args... 会返回什么类型?"

C++17 引入,等价于 typename std::invoke_result<F, Args...>::type

6.1.2.打包任务

c 复制代码
using R = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<R()>>(
    std::bind(std::forward<F>(f), std::forward<Args>(args)...));
  • std::packaged_task<R()> ------ 零参可调用的打包器
    packaged_task 的模板参数是函数签名。R() 表示"无参数、返回 R"。
  • std::bind ------ 参数绑定 + 值类别转发
c 复制代码
std::bind(std::forward<F>(f), std::forward<Args>(args)...)

bind 会拷贝或移动所有实参到内部存储,生成一个新的可调用对象。

6.1.3.为什么用 std::make_shared 而不是直接放 packaged_task?

c 复制代码
std::queue<std::function<void()>> tasks;

std::function<void()> 要求可调用对象可拷贝。而:

  • std::packaged_task 是只可移动、不可拷贝的
  • std::bind 绑定出 lambda 也可能因捕获移动-only 类型而不可拷贝。

解决方案:用 shared_ptr 做一层间接------shared_ptr 本身可拷贝,拷贝的只是控制块引用计数:

c 复制代码
auto task = std::make_shared<std::packaged_task<R()>>(
    std::bind(std::forward<F>(f), std::forward<Args>(args)...));

// 队列里存的 lambda 捕获 shared_ptr(可拷贝)
tasks.emplace([task] { (*task)(); });

生命周期问题也随之解决:submit 返回后局部 task 销毁,但队列中的 lambda 还持有一份引用,任务对象活到执行完毕。若没有这层 shared_ptr,任务对象在 submit 结束时就销毁了。

6.1.4.获取 future

c 复制代码
std::future<R> fut = task->get_future();

future 只可移动,return fut; 触发移动构造/移动返回值优化,把读取端交还给调用者。

6.1.5.入队与通知

c 复制代码
{ std::lock_guard lk(mtx); tasks.emplace([task] { (*task)(); }); }
cv.notify_one();
  • 先加锁,放入,释放锁,再通知。可以包装通知唤醒等待者时,等待者可以立即获得锁。
  • 唤醒一个,是因为一次只放入一个任务。唤醒所有,会导致其余唤醒者无意义唤醒。
  • tasks.emplace(...) vs tasks.push(...)
    直接构造 lambda 进队列,相比 lambda 的临时对象构造+移动,是顺手的小优化。

7.std::async(异步运行)

三种启动策略

c 复制代码
auto f1 = std::async(func);                           // 默认:实现决定(可能 async 或 deferred)
auto f2 = std::async(std::launch::async, func);       // 强制新线程立即执行
auto f3 = std::async(std::launch::deferred, func);    // 延迟:调用 get()/wait() 时才在当前线程执行
auto f4 = std::async(std::launch::async | std::launch::deferred, func); // 任选其一

各策略语义

策略 执行时机 线程 特点
async 调用即启动 新线程 真正并行
deferred 首次 get()/wait() 调用 get 的线程 惰性求值,可能永远不执行
默认 未指定 未指定 不可移植,不要依赖

注意 deferred 的陷阱

c 复制代码
auto f = std::async(std::launch::deferred, [] { std::cout << "任务\n"; });
// 如果不调用 f.get(),任务永远不会执行
f.get();  // 此时才在当前线程执行

future 析构阻塞陷阱:

c 复制代码
void bad() {
    auto f = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(10s);
    });
    // f 离开作用域析构时会阻塞 ~10 秒!
}  // 阻塞点

异常传递:

c 复制代码
auto f = std::async([] { throw std::runtime_error("失败"); });
try { f.get(); }
catch (const std::exception& e) { /* 捕获到任务中的异常 */ }

async vs thread 的选择:

std::thread std::async
返回值 无(需自行用 promise) 有 future
异常处理 线程内未捕获异常 → terminate 异常经 future 传递
线程创建 手动管理 实现可复用线程(如线程池)
适用 长生命周期、手动精细控制 "想要个结果"的临时任务

8.整体关系图

c 复制代码
                    ┌─────────────────────────────────────────┐
                    │           共享状态 (shared state)          │
                    │         (由实现管理的引用计数对象)          │
                    └──────▲──────────────▲──────────────▲─────┘
                           │              │              │
        写入端(三选一)      │              │              │   读取端
   ┌───────────────────────┼──────────────┼──────────────┼──────────────┐
   │                       │              │              │              │
std::promise          std::packaged_task   std::async    std::future   std::shared_future
set_value()           包装可调用对象,       启动策略 +      独占读取       共享读取
set_exception()       调用时自动写结果       自动绑定         get()一次      get()多次
   │                       │              │              │
   └───────────────────────┴──────────────┴──────────────┘
        底层机制:promise 是地基;packaged_task 和 async 内部都基于它

9.如何选择

需求 推荐组件
线程间等待条件 condition_variable + mutex
需要与 shared_mutex 等非常规锁配合等待 condition_variable_any
提交任务到线程池并取结果 packaged_task + 手写队列
快速启动异步任务取结果 std::async
手动创建线程传递结果/异常 std::promise
一个结果多个消费者 std::shared_future
一对一传递结果 std::future

经验法则:

  • 能用高层抽象就不用底层------async 优先于手写 thread + promise;
  • packaged_task 优先于在线程函数里手动 set_value
  • 带谓词的 cv.wait(lock, pred) 优先于裸 while 循环。
相关推荐
蛋先生DX2 小时前
内存安全翻车现场的罪魁祸首,根源就在这三步里?
c++·后端·编程语言
小七在进步2 小时前
C++入门(1)
java·jvm·c++
晴天的雨.9922 小时前
[C++算法]快乐数
数据结构·c++·算法
影视飓风TIM2 小时前
C++11 核心新特性完整梳理
数据结构·c++·算法
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】Ollama本地大语言模型部署
c++·人工智能·语言模型·自然语言处理·面试
晴天的雨.9922 小时前
[C++]算法双指针 复写0
数据结构·c++·算法
啦啦啦啦啦zzzz3 小时前
Appender抽象类的实现c++20
linux·服务器·c++·c++20
神仙别闹12 小时前
基于 C++ 实现两个有序链表序列的交集
java·c++·链表
hansang_IR14 小时前
【题解】[APIO2023] 赛博乐园 / cyberland
c++·算法·图论