同步
等待事件
持续等待
当一个线程等待另一个线程完成任务时,持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。但这样一方面等待线程会一直被阻塞,浪费了资源;而被等待线程如果被打断或者等待还会增加等待线程的等待时间。
周期间歇
当被等待线程要做其他事时,可以暂时释放互斥量:
bool flag;
std::mutex m;
void wait_for_flag()
{
std::unique_lock<std::mutex> lk(m);
while(!flag)
{
lk.unlock(); // 1 解锁互斥量
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 2 休眠100ms
lk.lock(); // 3 再锁互斥量
}
}
但是很难确定正确的休眠时间,并且要小心互斥量的变化。
条件变量
https://cppreference.cn/w/cpp/thread/condition_variable;
https://cppreference.cn/w/cpp/thread/condition_variable_any
这是优先选择,由另一线程来进行唤醒。C++标准库对条件变量有两套实现:std::condition_variable 和 std::condition_variable_any。这两个实现都包含在 <condition_variable> 头文件的声明中。两者都需要与一个互斥量一起才能工作;前者仅限于与 std::mutex 一起工作,而后者可以和任何满足最低标准的互斥量一起工作。 std::condition_variable_any 更加通用,不过考虑到开销,std::condition_variable 一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考虑 std::condition_variable_any。
以 std::condition_variable 为例,提供了两种类型的功能:等待和唤醒。
由于唤醒操作的不确定性,唤醒的线程有可能无法工作,因此推荐使用带条件谓词的 wait 。
wait(lock, predicate)。
执行逻辑如下:
- 持有锁检查谓词: wait 函数首先检查提供的 predicate (通常是一个 lambda 表达式)。此时传入的 lock 必须是锁定的状态。
- 如果谓词为 true: 说明条件已经满足,wait 函数直接返回。线程继续执行,lock 仍然保持锁定状态。
- 如果谓词为 false: 说明条件不满足,线程需要等待。此时 wait 执行一个原子操作序列:
a. 释放锁: wait原子调用 lock.unlock(),释放掉传入的 lock 所管理的互斥锁。这样就允许其他线程能够获取这个锁;
b. 阻塞线程: 当前线程进入阻塞状态,等待被 notify_one() 或 notify_all() 唤醒。 - 重新获取锁: 在唤醒后,wait 函数原子地尝试重新调用 lock.lock() 获取之前释放的互斥锁。直到成功获取锁为止。
- 再次检查谓词: 成功重新获取锁后,wait再次检查 predicate。
可能的死锁
根据 wait的流程,一般与 std::unique_lock <std::mutex> 配合使用,而不是 std::lock_guard<std::mutex>。 由于不能提前解锁,有可能导致死锁。
期望事件
“期望”(future)是c++标准库提供的一种同步机制,用于表示一个一次性异步操作的未来结果。相比起条件变量,它更强调线程之间数据的传递,而不需要复杂的同步机制。
| 期望 | 条件变量 | |
|---|---|---|
| 目的 | 线程间一次性传递值 | 线程间等待/通知机制 |
| 状态 | 事件发生后状态固定,不能重置 | 可多次触发 |
| 数据传递 | 直接传递 | 通过共享变量传递 |
| 使用场景 | 一次性事件,如任务结果 | 重复事件,如队列数据就绪 |
C++ 中通过两种模板类型实现:
std::future
std::shared_future
https://cppreference.cn/w/cpp/thread/shared_future
模板参数 T 为事件关联的数据类型,若无需传递数据,可使用 std::future<void>。
使用期望
如上所述, std::future 对象在内部存储一个将来会被某个 Provider 赋值的值,并提供了一个访问该值的机制:通过 get() 成员函数实现。如果试图在 get() 函数可用之前通过它来访问相关的值,那么将会阻塞,直到该值可用。
创建期望
一个有效的 std::future 对象通常由以下三种 Provider 创建:通过三种 Provider: std::async,std::packaged_task, std::promise。而提供的默认构造函数创建的 std::future 对象都是无效的。
std::async
std::async 是一个函数模板,用于启动异步任务并返回一个 std::future 对象,该对象用于保存异步操作的结果。实际上它封装了线程的创建和管理,使得异步编程更加简单和安全。
template< class F, class... Args >
std::future<result_of_T> async( std::launch policy(可选), F&& f, Args&&... args );
// policy:启动策略,指定任务执行的方式
// f:要异步执行的可调用对象(函数、Lambda 表达式、函数对象等)。
// Args:传递给可调用对象 f 的参数。
//传参方式同std::thread
提供三种启动策略:
- std::launch::async:立即执行,传递的函数会在一个新线程中立即执行。
- std::launch::deferred:延迟执行,延迟到返回的 future对象调用 get() 或者 wait() 执行,在调用者线程上执行。而如果不调用,就不会执行任务。
- std::launch::async | std::launch::deferred:默认行为。由系统确定使用哪种启动策略。
std::promise
https://cppreference.cn/w/cpp/thread/promise/set_value
类模板 std::promise<T> 提供了一个机制,它允许显式地设置一个值或异常,然后通过与之关联的 std::future 对象在其他线程中获取这个结果。这样就提供了一种简单的线程传递数据方法
以一个生产者线程、消费者线程协作为例:
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
//生产者线程:传递结果
void producer_task(std::promise<int> result_promise, int input) {
//耗时计算
std::this_thread::sleep_for(std::chrono::seconds(1));
//将结果设置到 promise 中(传递到消费者线程)
result_promise.set_value(square);
}
//消费者线程:等待结果并处理
void consumer_task(std::future<int> result_future) {
//阻塞等待结果(从生产者线程获取)
int result = result_future.get();
//处理结果
std::cout << "消费者线程收到结果: " << result << std::endl;
}
int main() {
//创建 promise 和 future
std::promise<int> prom;
std::future<int> fut = prom.get_future();
//启动生产者线程
std::thread producer(producer_task, std::move(prom), 5);
//启动消费者线程
std::thread consumer(consumer_task, std::move(fut));
//等待线程结束
producer.join();
consumer.join();
return 0;
}
注意事项:
- 在传递 promise 对象和 future对象时,需要使用 std::move,因为都禁止了拷贝。可以使用 std::ref 进行引用,但是要注意生命周期。
std::packaged_task
https://cppreference.cn/w/cpp/thread/packaged_task
template< class R, class ...ArgTypes >
class packaged_task<R(ArgTypes...)>;
它将一个可调用对象(函数、Lambda、函数对象等)包装成一个异步任务,并与一个 std::future 对象关联,用于获取该任务的执行结果。使用方式类似 **std::promise*。
对比
| 特性 | std::promise |
std::async |
std::packaged_task |
|---|---|---|---|
| 抽象级别 | 低级(手动控制) | 高级(自动管理) | 中级(函数包装器) |
| 易用性 | 复杂 | 简单 | 中等 |
| 控制粒度 | 高(完全控制) | 低(自动管理) | 中(控制执行时机) |
| 线程管理 | 手动 | 自动 | 手动 |
| 适用场景 | 复杂同步、自定义逻辑 | 简单异步调用 | 任务队列、线程池 |
| 函数包装 | 无,需手动调用函数 | 自动包装函数 | 显式包装函数 |
| 结果传递 | 手动设置值/异常 | 自动传递返回值/异常 | 自动传递返回值/异常 |
| 执行时机 | 完全自定义 | 立即或延迟(取决于策略) | 可控制执行时机 |
| 移动语义 | 必须使用std::move | 自动处理 | 必须使用std::move |
| 异常处理 | 手动设置异常 | 自动捕获并传递 | 自动捕获并传递 |
| 性能开销 | 低 | 中(可能创建线程) | 低 |
待更新
这一节写的很乱,感觉主要是还是没有太多使用过这些工具。以后慢慢记录更新吧

浙公网安备 33010602011771号