文章目录
-
- 4.1等待事件或条件
- 4.1.1等待条件达成
- 4.1.2 构建线程安全队列
- 4.2 使用 `std::future`
- 4.2.1 后台任务的返回值
- 4.2.2 `std::future` 与任务关联
- 4.2.3 使用 `std::promise`
- 4.2.4 将异常存储到 `std::future` 中
- 4.2.5 多个线程的等待
-
- **解决方案:使用 `std::shared_future`**
- **应用场景示例**
- **`std::shared_future` 的构造与状态转移**
- **简化类型推导**
- **超时等待**
- **总结**
- **完整代码示例**
- **代码说明**
- **运行结果示例**
- **关键点解析**
- 4.3 限时等待
- 总结
- **示例 1:线程休眠 (`std::this_thread::sleep_for`)**
- **示例 2:条件变量超时等待 (`std::condition_variable`)**
- **示例 3:`std::future` 的 `wait_for` 和 `wait_until`**
- **示例 4:互斥锁的超时尝试 (`std::timed_mutex`)**
- **示例 5:计时代码块执行时间**
- 4.4 简化代码
- **快速排序——FP 模式版**
- **快速排序——FP 模式线程强化版**
- **代码 4.14:`spawn_task` 的简单实现**
- **优化与讨论**
- 总结
- **完整代码:并行快速排序**
- **代码说明**
- **运行结果**
- **优化与注意事项**
- 4.4.2 使用消息传递的同步操作
- **代码说明**
- **参与者模式(Actor Model)**
- **总结**
- **完整代码:基于消息传递的 ATM 系统**
- **代码说明**
- **运行结果**
- **总结**
- **完整代码:基于持续性连接的用户登录**
- **代码说明**
- **运行结果**
- **关键点**
- 4.4.5 等待多个 Future
- **总结**
- **完整代码:基于 `std::experimental::when_all` 和 `std::experimental::when_any` 的多任务处理**
- **代码说明**
- **运行结果**
- **关键点**
- 4.4.6 使用 `std::experimental::when_any` 等待第一个 Future
- **代码示例:使用 `std::experimental::when_any` 处理第一个找到的值**
- **代码说明**
- **优化叙述**
- **运行结果示例**
- **补充说明**
- **完整代码:使用 `std::experimental::when_any` 等待第一个完成的任务**
- **代码说明**
- **运行结果示例**
- **关键点**
- 4.4.7 锁存器和栅栏
- **4.4.8 `std::experimental::latch`:基础的锁存器类型**
- **4.4.9 `std::experimental::barrier`:简单的栅栏**
- **锁存器与栅栏的区别**
- **总结**
- **完整代码:使用 `std::experimental::latch` 和 `std::experimental::barrier`**
- **代码说明**
- **运行结果示例**
- **关键点**
- 4.4.10 `std::experimental::flex_barrier` — 更灵活和更友好的栅栏
- **代码示例:使用 `std::experimental::flex_barrier` 管理串行部分**
- **代码说明**
- **与 `std::experimental::barrier` 的区别**
- **优化后的优点**
- **应用场景**
- **完整代码:使用 `std::experimental::flex_barrier` 实现动态线程管理**
- **代码说明**
- **运行结果示例**
- **关键点**
4.1等待事件或条件
在夜间运行的火车上,如何确保在正确的站点下车是一个有趣的问题。我们可以通过几种不同的方法来解决这个问题,这些方法与多线程编程中的等待机制有着惊人的相似之处。以下是对这些方法的详细解释和类比:
1. 保持清醒,持续检查
方法描述:整晚保持清醒,每到一个站点都检查是否到达目的地。这样虽然不会错过站点,但会导致极度疲倦。
线程类比:在多线程编程中,这种方式类似于忙等待(busy-waiting)。线程会持续检查某个共享标志(通常是一个互斥量保护的变量),直到另一个线程完成任务并重置该标志。这种方式会消耗大量的CPU资源,因为线程在等待期间一直在运行,并且会阻止其他线程获取锁,导致系统效率低下。
代码示例:
bool flag;
std::mutex m;
void wait_for_flag() {
std::unique_lock<std::mutex> lk(m);
while (!flag) {
// 持续检查标志
}
}
2. 设置闹钟,定时检查
方法描述:查看火车时刻表,估算到达目的地的时间,并设置一个稍早的闹钟。这样可以在大部分时间休息,但存在火车晚点或闹钟电池耗尽的风险。
线程类比:这种方式类似于在等待线程中使用周期性休眠。线程在检查标志的间隙进行短暂的休眠,以减少CPU的占用率。虽然这种方法比忙等待更高效,但仍然存在休眠时间难以确定的问题。休眠时间过短会导致CPU资源浪费,过长则可能导致响应延迟。
代码示例:
bool flag;
std::mutex m;
void wait_for_flag() {
std::unique_lock<std::mutex> lk(m);
while (!flag) {
lk.unlock(); // 解锁互斥量
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 休眠100ms
lk.lock(); // 重新锁定互斥量
}
}
3. 使用条件变量,等待事件触发
方法描述:理想的方式是,当火车到达目的地时,有人或其他东西能够自动唤醒你。这样既不需要保持清醒,也不需要担心闹钟的准确性。
线程类比:这种方式类似于使用条件变量(condition variable)。条件变量允许线程在等待某个条件达成时进入休眠状态,直到另一个线程通知它条件已经满足。这种方式是最高效的,因为它避免了忙等待和周期性休眠的缺点,线程只在必要时被唤醒。
代码示例:
std::mutex m;
std::condition_variable cv;
bool flag = false;
void wait_for_flag() {
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{
return flag; }); // 等待条件变量通知
}
void set_flag() {
std::unique_lock<std::mutex> lk(m);
flag = true;
cv.notify_all(); // 通知所有等待的线程
}
总结
- 保持清醒,持续检查:类似于忙等待,效率低下,消耗大量资源。
- 设置闹钟,定时检查:类似于周期性休眠,比忙等待高效,但难以确定最佳休眠时间。
- 使用条件变量,等待事件触发:类似于自动唤醒机制,是最高效的方式,线程只在必要时被唤醒。
在多线程编程中,使用条件变量是最优的选择,因为它能够有效地管理线程的等待和唤醒,避免资源浪费和响应延迟。这与在火车上使用自动唤醒机制来确保在正确站点下车的方式非常相似。
下面是一个完整的C++代码示例,展示了如何使用条件变量来实现线程间的等待与通知机制。这个例子模拟了一个简单的场景:一个线程等待另一个线程完成任务并发出通知。
完整代码示例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>
// 全局变量
std::mutex mtx; // 互斥量,用于保护共享数据
std::condition_variable cv; // 条件变量,用于线程间通信
bool ready = false; // 标志位,表示任务是否完成
// 等待任务的线程
void wait_for_task() {
std::unique_lock<std::mutex> lock(mtx); // 加锁
std::cout << "等待线程:等待任务完成..." << std::endl;
// 使用条件变量等待,直到 ready 为 true
cv.wait(lock, [] {
return ready; });
std::cout << "等待线程:任务已完成,继续执行!" << std::endl;
}
// 执行任务的线程
void perform_task() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟任务执行时间
std::unique_lock<std::mutex> lock(mtx); // 加锁
std::cout << "执行线程:任务已完成!" << std::endl;
ready = true; // 设置标志位为 true
cv.notify_one(); // 通知等待的线程
}
int main() {
std::cout << "主线程:启动等待线程和执行线程..." << std::endl;
std::thread t1(wait_for_task); // 创建等待线程
std::thread t2(perform_task); // 创建执行线程
t1.join(); // 等待等待线程结束
t2.join(); // 等待执行线程结束
std::cout << "主线程:所有线程已完成,程序结束。" << std::endl;
return 0;
}
代码说明
-
全局变量:
mtx
:互斥量,用于保护共享数据(ready
标志)。cv
:条件变量,用于线程间通信。ready
:布尔标志,表示任务是否完成。
-
等待线程 (
wait_for_task
):- 使用
std::unique_lock
加锁。 - 调用
cv.wait(lock, predicate)
,等待条件变量通知。predicate
是一个 lambda 函数,用于检查ready
是否为true
。 - 如果
ready
为false
,线程会释放锁并进入休眠状态,直到被通知。 - 当
ready
为true
时,线程被唤醒并继续执行。
- 使用
-
执行线程 (
perform_task
):- 模拟任务执行(休眠 2 秒)。
- 加锁后,设置
ready
为true
,并调用cv.notify_one()
通知等待的线程。
-
主线程:
- 创建并启动等待线程和执行线程。
- 使用
join()
等待两个线程完成。
输出结果
运行程序后,输出如下:
主线程:启动等待线程和执行线程...
等待线程:等待任务完成...
执行线程:任务已完成!
等待线程:任务已完成,继续执行!
主线程:所有线程已完成,程序结束。
关键点
-
条件变量的使用:
cv.wait(lock, predicate)
:线程在等待时会释放锁,避免忙等待。cv.notify_one()
:通知一个等待的线程。
-
互斥量的作用:
- 保护共享数据(
ready
标志),避免竞争条件。
- 保护共享数据(
-
线程同步:
- 等待线程在任务完成前会休眠,不会浪费 CPU 资源。
- 执行线程完成任务后,会通知等待线程继续执行。
类比火车下车的场景
- 等待线程:就像在火车上睡觉的乘客,等待到达目的地。
- 执行线程:就像火车驾驶员,负责完成任务(到达站点)。
- 条件变量:就像火车到站时的自动唤醒机制,确保乘客在正确的时间醒来。
这种方式避免了忙等待和周期性检查的低效问题,是最优的解决方案。
4.1.1等待条件达成
以下是关于 std::condition_variable
和 std::condition_variable_any
的详细说明,以及如何使用 std::condition_variable
实现线程间数据传递的完整代码示例和解释。代码已经排版并优化了叙述。
1. std::condition_variable
和 std::condition_variable_any
C++ 标准库提供了两种条件变量实现:
std::condition_variable
:只能与std::mutex
一起使用,性能较高,是首选。std::condition_variable_any
:可以与任何符合互斥量要求的锁类型一起使用,更加灵活,但性能开销较大。
通常情况下,优先选择 std::condition_variable
,除非需要与非常规的互斥量配合使用。
2. 使用 std::condition_variable
实现线程间数据传递
以下代码展示了如何使用 std::condition_variable
实现一个生产者-消费者模型。生产者线程准备数据并将其推入队列,消费者线程从队列中取出数据并处理。
代码 4.1:使用 std::condition_variable
处理数据等待
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
// 全局变量
std::mutex mut; // 互斥量,保护共享数据
std::queue<int> data_queue; // 数据队列
std::condition_variable data_cond; // 条件变量,用于线程间通信
// 数据准备线程(生产者)
void data_preparation_thread() {
for (int i = 0; i < 10; ++i) {
int data = i; // 模拟数据准备
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟数据准备时间
{
std::lock_guard<std::mutex> lk(mut); // 加锁
data_queue.push(data); // 将数据推入队列
std::cout << "生产者:数据 " << data << " 已准备\n";
}
data_cond.notify_one(); // 通知一个等待的线程
}
}
// 数据处理线程(消费者)
void data_processing_thread() {
while (true) {
std::unique_lock<std::mutex> lk(mut); // 加锁
// 等待条件满足:队列不为空
data_cond.wait(lk, [] {
return !data_queue.empty(); });
// 条件满足,取出数据
int data = data_queue.front();
data_queue.pop();
lk.unlock(); // 解锁,允许其他线程操作队列
std::cout << "消费者:处理数据 " << data << "\n";
// 模拟数据处理时间
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// 如果处理完最后一个数据,退出循环
if (data == 9) {
break;
}
}
}
int main() {
std::cout << "主线程:启动生产者和消费者线程...\n";
std::thread producer(data_preparation_thread); // 生产者线程
std::thread consumer(data_processing_thread); // 消费者线程
producer.join(); // 等待生产者线程结束
consumer.join(); // 等待消费者线程结束
std::cout << "主线程:所有线程已完成,程序结束。\n";
return 0;
}
3. 代码说明
生产者线程 (data_preparation_thread
)
- 模拟数据准备过程,生成数据并推入队列。
- 使用
std::lock_guard
加锁,确保对队列的操作是线程安全的。 - 调用
data_cond.notify_one()
通知等待的消费者线程。
消费者线程 (data_processing_thread
)
- 使用
std::unique_lock
加锁,确保对队列的操作是线程安全的。 - 调用
data_cond.wait(lk, predicate)
等待条件满足(队列不为空)。predicate
是一个 lambda 函数,检查队列是否为空。- 如果队列为空,线程会释放锁并进入休眠状态,直到被通知。
- 当条件满足时,从队列中取出数据并处理。
- 处理完数据后,解锁互斥量,允许其他线程操作队列。
主线程
- 启动生产者和消费者线程。
- 使用
join()
等待两个线程完成。
4. 关键点
条件变量的使用
wait(lk, predicate)
:线程在等待时会释放锁,避免忙等待。当条件满足时,线程被唤醒并重新获取锁。notify_one()
:通知一个等待的线程。
互斥量的作用
- 保护共享数据(
data_queue
),避免竞争条件。
std::unique_lock
的灵活性
- 允许在等待期间解锁互斥量,并在条件满足时重新加锁。
- 与
std::lock_guard
不同,std::unique_lock
更加灵活,适合与条件变量一起使用。
5. 输出示例
运行程序后,输出如下:
主线程:启动生产者和消费者线程...
生产者:数据 0 已准备
消费者:处理数据 0
生产者:数据 1 已准备
消费者:处理数据 1
...
生产者:数据 9 已准备
消费者:处理数据 9
主线程:所有线程已完成,程序结束。
6. 总结
std::condition_variable
是线程间同步的强大工具,适合用于生产者-消费者模型。- 通过条件变量,可以避免忙等待和周期性检查的低效问题。
std::unique_lock
提供了灵活的锁管理,适合与条件变量一起使用。- 代码 4.1 是一个典型的线程安全队列实现,可以扩展到更复杂的场景中。
以下是关于构建线程安全队列的详细说明和完整代码实现,已经排版并优化了叙述。
4.1.2 构建线程安全队列
1. 线程安全队列的设计目标
在设计线程安全队列时,我们需要解决以下问题:
- 条件竞争:多个线程可能同时访问队列,导致数据不一致。
- 等待机制:消费者线程需要等待队列中有数据时才能继续执行。
- 灵活性:提供多种操作接口,如
try_pop
和wait_and_pop
,以适应不同的使用场景。
2. std::queue
的接口分析
std::queue
的接口主要包括以下操作:
- 查询队列状态:
empty()
和size()
。 - 访问队列元素:
front()
和back()
。 - 修改队列:
push()
、pop()
和emplace()
。
为了实现线程安全,我们需要将 front()
和 pop()
合并为一个操作,以避免条件竞争。
3. 线程安全队列的接口设计
线程安全队列的接口设计如下:
push(T new_value)
:向队列中添加数据。try_pop(T& value)
:尝试从队列中弹出数据,如果队列为空则返回false
。try_pop()
:尝试从队列中弹出数据,返回std::shared_ptr<T>
,如果队列为空则返回nullptr
。wait_and_pop(T& value)
:等待队列中有数据时弹出数据。wait_and_pop()
:等待队列中有数据时弹出数据,返回std::shared_ptr<T>
。empty()
:检查队列是否为空。
4. 线程安全队列的实现
以下是线程安全队列的完整实现代码:
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue {
private:
mutable std::mutex mut; // 互斥量,必须是可变的
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue() {
}
// 拷贝构造函数
threadsafe_queue(const threadsafe_queue& other) {
std::lock_guard<std::mutex> lk(other.mut);
data_queue = other.data_queue;
}
// 禁止赋值操作
threadsafe_queue& operator=(const threadsafe_queue&) = delete;
// 向队列中添加数据
void push(T new_value) {
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one(); // 通知一个等待的线程
}
// 尝试从队列中弹出数据(存储到引用参数中)
bool try_pop(T& value) {
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty()) {
return false;
}
value = data_queue.front();
data_queue.pop();
return true;
}
// 尝试从队列中弹出数据(返回 shared_ptr)
std::shared_ptr<T> try_pop() {
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty()) {
return std::shared_ptr<T>();
}
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
// 等待队列中有数据时弹出数据(存储到引用参数中)
void wait_and_pop(T& value) {
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {
return !data_queue.empty(); });
value = data_queue.front();
data_queue.pop();
}
// 等待队列中有数据时弹出数据(返回 shared_ptr)
std::shared_ptr<T> wait_and_pop() {
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {
return !data_queue.empty(); });
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
// 检查队列是否为空
bool empty() const {
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};
5. 代码说明
关键点
-
互斥量 (
std::mutex
):- 用于保护共享数据(
data_queue
)。 - 必须是
mutable
,以便在const
成员函数(如empty()
)中加锁。
- 用于保护共享数据(
-
条件变量 (
std::condition_variable
):- 用于实现等待机制,消费者线程在队列为空时进入休眠状态。
- 当生产者线程调用
push()
时,会通知一个等待的消费者线程。
-
wait_and_pop
的实现:- 使用
std::unique_lock
,允许在等待期间解锁互斥量。 - 通过
data_cond.wait()
等待队列不为空的条件。
- 使用
-
try_pop
的实现:- 如果队列为空,立即返回
false
或nullptr
。 - 否则,弹出数据并返回。
- 如果队列为空,立即返回
-
拷贝构造函数:
- 加锁保护,确保拷贝操作是线程安全的。
6. 使用示例
以下是使用 threadsafe_queue
的示例代码:
#include <iostream>
#include <thread>
threadsafe_queue<int> queue;
void producer() {
for (int i = 0; i < 10; ++i) {
queue.push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "生产者:数据 " << i << " 已添加\n";
}
}
void consumer() {
while (true) {
int value;
queue.wait_and_pop(value);
std::cout << "消费者:处理数据 " << value << "\n";
if (value == 9) {
break;
}
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
std::cout << "主线程:程序结束。\n";
return 0;
}
7. 输出示例
运行程序后,输出如下:
生产者:数据 0 已添加
消费者:处理数据 0
生产者:数据 1 已添加
消费者:处理数据 1
...
生产者:数据 9 已添加
消费者:处理数据 9
主线程:程序结束。
8. 总结
- 线程安全队列通过互斥量和条件变量实现了线程间的安全数据传递。
wait_and_pop
提供了等待机制,适合消费者线程。try_pop
提供了非阻塞的操作,适合需要立即返回的场景。- 该实现可以扩展到更复杂的生产者和消费者模型中。
4.2 使用 std::future
假设你计划乘飞机去国外度假。当你到达机场并完成登机手续后,还需要等待机场广播通知登机时间。在这段时间里,你可能会在候机室做一些事情来打发时间,比如读书、上网或喝一杯咖啡。然而,你的核心目标是等待一件事情:机场广播通知登机。
C++ 标准库将这种事件建模为 future
。当一个线程需要等待某个特定事件发生时,它实际上是在等待一个期望的结果。之后,线程会周期性地(通常是较短的时间间隔)检查该事件是否已经触发(例如查看信息板),同时还可以执行其他任务(例如品尝昂贵的咖啡)。此外,线程也可以先执行其他任务,直到对应的任务完成,此时 future
的状态会变为“就绪”。需要注意的是,future
一旦变为就绪状态,就不能被重置。
C++ 中的两种 Future
C++ 标准库提供了两种 future
类型,声明在 <future>
头文件中:
-
std::future<>
:- 表示唯一的未来事件。
- 每个
std::future
对象只能与一个特定事件相关联。
-
std::shared_future<>
:- 可以被多个线程共享,允许多个实例同时变为就绪状态。
- 所有共享的实例可以访问与事件相关的数据。
- 其行为类似于
std::shared_ptr
,但用于表示共享的未来事件。
这两种类型的模板参数定义了与事件相关联的数据类型。如果事件不涉及具体数据(例如仅通知事件的发生),可以使用特化模板 std::future<void>
和 std::shared_future<void>
。
同步访问注意事项
尽管 std::future
和 std::shared_future
提供了异步操作的结果管理功能,但它们本身并不提供同步机制。如果多个线程需要访问同一个独立的 std::future
对象,则必须通过互斥量或其他同步机制进行保护。然而,对于 std::shared_future
,由于其设计允许多个线程安全地访问同一个异步结果的不同副本,因此在这种情况下不需要额外的同步。
实验性扩展
C++ 并行技术规范在 std::experimental
命名空间中对 std::future
和 std::shared_future
进行了扩展,提供了增强功能。这些实验性的模板类位于 <experimental/future>
头文件中,旨在与标准命名空间中的实现区分开来。
需要注意的是,std::experimental
命名空间中的类和函数并非正式标准的一部分,其语法和语义可能在未来纳入 C++ 标准时发生变化。因此,在使用这些实验性功能时需谨慎。
简单的应用场景:后台计算
在第 2 章中提到,std::thread
执行的任务无法直接返回值。然而,这个问题可以通过 std::future
来解决。std::future
提供了一种机制,允许主线程等待后台线程完成计算,并获取其结果。
以下是一个简单的例子,展示了如何使用 std::future
来处理后台计算任务:
#include <iostream>
#include <future>
#include <thread>
// 后台计算任务
int compute(int value) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算
return value * 2;
}
int main() {
// 启动后台任务
std::future<int> result = std::async(std::launch::async, compute, 42);
// 主线程可以在此期间执行其他任务
std::cout << "Waiting for the result..." << std::endl;
// 等待后台任务完成并获取结果
int computedValue = result.get(); // 阻塞直到结果可用
std::cout << "Computed value: " << computedValue << std::endl;
return 0;
}
代码解析
-
启动后台任务:
- 使用
std::async
启动一个异步任务,返回一个std::future
对象。 - 参数
std::launch::async
确保任务在单独的线程中运行。
- 使用
-
主线程继续执行:
- 在等待后台任务完成期间,主线程可以执行其他任务。
-
获取结果:
- 调用
result.get()
阻塞主线程,直到后台任务完成并返回结果。
- 调用
总结
std::future
和std::shared_future
是 C++ 标准库中用于处理异步操作的强大工具。- 它们允许线程等待特定事件的发生,并在事件完成后获取相关数据。
std::future
是独占的,而std::shared_future
支持共享访问。- 在多线程环境中,需要注意同步问题,尤其是在访问独立的
std::future
对象时。 - 实验性扩展提供了更多功能,但需注意其非标准化特性。
通过合理使用 std::future
和 std::shared_future
,可以显著简化异步编程模型,提升程序的并发性能和可维护性。
4.2.1 后台任务的返回值
在某些场景下,我们需要执行一个耗时计算任务,但并不迫切需要其结果。虽然可以使用 std::thread
来启动新线程执行计算,但 std::thread
并不提供直接接收返回值的机制。为了解决这一问题,C++ 标准库提供了 std::async
函数模板(声明在 <future>
头文件中),用于启动异步任务并获取返回值。
std::async
的基本用法
当不需要立即获取任务结果时,可以使用 std::async
启动一个异步任务。与 std::thread
不同,std::async
会返回一个 std::future
对象,该对象持有最终计算结果。当我们需要结果时,只需调用 std::future
的 get()
成员函数,这将阻塞当前线程直到任务完成,并返回计算结果。
示例代码:
#include <future>
#include <iostream>
int find_the_answer_to_ltuae(); // 假设这是一个耗时计算函数
void do_other_stuff(); // 假设这是一个其他任务函数
int main() {
// 启动异步任务
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
// 执行其他任务
do_other_stuff();
// 获取异步任务的结果
std::cout << "The answer is " << the_answer.get() << std::endl;
}
向异步任务传递参数
std::async
支持通过额外参数向目标函数传递数据。以下是几种常见的情况:
-
普通函数:
- 参数会被传递给目标函数。
- 如果参数是右值,则会通过移动操作传递,避免不必要的拷贝。
-
成员函数:
- 第一个参数是指向成员函数的指针。
- 第二个参数是类的具体实例(可以通过指针或
std::ref
包装)。 - 剩余参数作为成员函数的参数。
-
可调用对象(如 lambda 表达式或函数对象):
- 参数会作为可调用对象的构造参数或调用参数传递。
示例代码:
#include <string>
#include <future>
#include <functional>
struct X {
void foo(int, const std::string&);
std::string bar(const std::string&);
};
X x;
// 调用成员函数 foo
auto f1 = std::async(&X::foo, &x, 42, "hello"); // 调用 p->foo(42, "hello"),p 是指向 x 的指针
// 调用成员函数 bar
auto f2 = std::async(&X::bar, x, "goodbye"); // 调用 tmpx.bar("goodbye"),tmpx 是 x 的拷贝副本
struct Y {
double operator()(double);
};
Y y;
// 调用函数对象
auto f3 = std::async(Y(), 3.141); // 调用 tmpy(3.141),tmpy 通过 Y 的移动构造函数得到
auto f4 = std::async(std::ref(y), 2.718); // 调用 y(2.718)
// 调用全局函数或函数对象
X baz(X&);
auto f5 = std::async(baz, std::ref(x)); // 调用 baz(x)
// 使用“只移动”类型的函数对象
class move_only {
public:
move_only() = default;
move_only(move_only&&) = default;
move_only(const move_only&) = delete;
move_only& operator=(move_only&&) = default;
move_only& operator=(const move_only&) = delete;
void operator()();
};
auto f6 = std::async(move_only()); // 调用 tmp(),tmp 通过 std::move(move_only()) 构造得到
控制任务的启动方式
std::async
的行为取决于任务是否需要立即启动,或者是否可以延迟执行。我们可以通过向 std::async
传递一个额外参数来指定任务的启动策略。这个参数的类型是 std::launch
,具体选项如下:
-
std::launch::deferred
:- 表示任务的执行会被延迟到调用
wait()
或get()
时才开始。
- 表示任务的执行会被延迟到调用
-
std::launch::async
:- 表示任务必须在其独立的线程上执行。
-
std::launch::deferred | std::launch::async
:- 表示实现可以选择上述两种方式之一,默认情况下会选择这种方式。
示例代码:
auto f7 = std::async(std::launch::async, Y(), 1.2); // 在新线程上执行
auto f8 = std::async(std::launch::deferred, baz, std::ref(x)); // 在 wait() 或 get() 调用时执行
auto f9 = std::async(std::launch::deferred | std::launch::async, baz, std::ref(x)); // 实现选择执行方式
auto f10 = std::async(baz, std::ref(x)); // 默认行为
f8.wait(); // 触发延迟任务的执行
std::future
的等待行为
std::future
的等待行为取决于任务的实际启动方式。如果任务被标记为延迟执行(std::launch::deferred
),则只有在调用 wait()
或 get()
时才会真正开始执行任务。
替代方案:std::packaged_task
和 std::promise
除了 std::async
,还有其他方式可以将任务与 std::future
关联起来:
-
std::packaged_task
:- 提供了更高层次的抽象,封装了一个可调用对象,并允许通过
std::future
获取其返回值。
- 提供了更高层次的抽象,封装了一个可调用对象,并允许通过
-
std::promise
:- 提供了一种显式设置
std::future
值的方式,适用于更复杂的场景。
- 提供了一种显式设置
由于 std::packaged_task
的抽象级别更高且更易于使用,因此通常优先选择它。
总结
std::async
是一种简单而强大的工具,用于启动异步任务并获取返回值。- 它支持多种参数传递方式,包括普通函数、成员函数和可调用对象。
- 可以通过
std::launch
控制任务的启动策略,选择立即执行或延迟执行。 - 替代方案包括
std::packaged_task
和std::promise
,适用于更复杂的需求。
通过合理使用这些工具,可以显著简化异步编程模型,提升程序的并发性能和可维护性。
以下是一个完整的代码示例,展示了如何使用 std::async
启动异步任务并获取返回值。该示例包括了普通函数、成员函数和可调用对象的使用方式,并演示了如何通过 std::launch
控制任务的启动策略。
完整代码示例
#include <iostream>
#include <future>
#include <string>
#include <thread>
#include <chrono>
// 普通函数
int find_the_answer_to_life() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算
return 42;
}
// 成员函数示例
struct X {
void foo(int value, const std::string& message) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "X::foo: Value = " << value << ", Message = " << message << std::endl;
}
int bar(int value) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return value * 2;
}
};
// 可调用对象示例
struct Y {
double operator()(double value) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return value * 3.14;
}
};
// 全局函数示例
std::string baz(const std::string& input) {
std::this_thread::sleep_for(std::chrono::seconds(1));
return "Processed: " + input;
}
int main() {
// 使用 std::async 调用普通函数
std::future<int> future1 = std::async(find_the_answer_to_life);
std::cout << "Async task 'find_the_answer_to_life' started." << std::endl;
// 使用 std::async 调用成员函数
X x;
auto future2 = std::async(&X::bar, &x, 10); // 调用 x.bar(10)
std::cout << "Async task 'X::bar' started." << std::endl;
// 使用 std::async 调用可调用对象
Y y;
auto future3 = std::async(Y(), 2.5); // 调用 Y()(2.5)
std::cout << "Async task 'Y()' started." << std::endl;
// 使用 std::async 调用全局函数
auto future4 = std::async(baz, "Hello, Async!");
std::cout << "Async task 'baz' started." << std::endl;
// 使用 std::launch 控制任务启动策略
auto future5 = std::async(std::launch::async, find_the_answer_to_life); // 强制在新线程上执行
auto future6 = std::async(std::launch::deferred, baz, "Deferred Task"); // 延迟执行
// 执行其他任务
std::cout << "Doing other work..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
// 获取异步任务的结果
std::cout << "The answer to life is: " << future1.get() << std::endl; // 阻塞直到任务完成
std::cout << "Result of X::bar: " << future2.get() << std::endl;
std::cout << "Result of Y(): " << future3.get() << std::endl;
std::cout << "Result of baz: " << future4.get() << std::endl;
std::cout << "Deferred task result: " << future6.get() << std::endl; // 触发延迟任务
return 0;
}
代码说明
1. 普通函数
find_the_answer_to_life()
是一个模拟耗时计算的普通函数。- 使用
std::async
启动该函数,并通过std::future
获取其返回值。
2. 成员函数
- 定义了一个结构体
X
,包含两个成员函数:foo
:不返回值,仅打印信息。bar
:返回计算结果。
- 使用
std::async
调用成员函数bar
,并通过指针或引用传递对象实例。
3. 可调用对象
- 定义了一个结构体
Y
,重载了operator()
,使其成为可调用对象。 - 使用
std::async
调用该对象,并通过移动构造函数传递参数。
4. 全局函数
- 定义了一个全局函数
baz
,用于处理字符串输入。 - 使用
std::async
调用该函数。
5. 控制任务启动策略
- 使用
std::launch::async
强制任务在独立线程上执行。 - 使用
std::launch::deferred
延迟任务执行,直到调用get()
或wait()
。
运行结果示例
假设程序运行时线程调度正常,可能的输出如下:
Async task 'find_the_answer_to_life' started.
Async task 'X::bar' started.
Async task 'Y()' started.
Async task 'baz' started.
Doing other work...
X::foo: Value = 42, Message = hello
The answer to life is: 42
Result of X::bar: 20
Result of Y(): 7.85
Result of baz: Processed: Hello, Async!
Deferred task result: Processed: Deferred Task
关键点解析
-
std::async
的灵活性:- 支持普通函数、成员函数和可调用对象。
- 可以通过
std::launch
控制任务启动策略。
-
std::future
的阻塞行为:- 调用
get()
会阻塞当前线程,直到异步任务完成。
- 调用
-
延迟任务的执行:
- 使用
std::launch::deferred
可以延迟任务执行,适合需要手动控制的任务。
- 使用
通过这个示例,您可以清楚地了解如何使用 std::async
启动异步任务,并结合 std::future
获取返回值。
4.2.2 std::future
与任务关联
std::packaged_task
是 C++ 标准库中用于将 std::future
与函数或可调用对象绑定的工具。当调用 std::packaged_task
对象时,会执行绑定的函数或可调用对象,并将返回值存储在 std::future
中,供后续获取。这种机制常用于构建线程池(见第 9 章)或其他任务管理场景,例如在任务所在线程上运行其他任务,或将它们串行化运行在一个特殊的后台线程上。
应用场景
当一个较大的操作被分解为多个独立的子任务时,每个子任务可以封装到一个 std::packaged_task
实例中,然后传递给任务调度器或线程池。这种方式对任务细节进行了抽象,调度器只需处理 std::packaged_task
实例,而无需直接处理单独的函数。
std::packaged_task
的定义与模板参数
std::packaged_task
的模板参数是一个函数签名,例如:
void()
表示没有参数且没有返回值的函数。int(std::string&, double*)
表示接受一个非const
引用的std::string
参数和一个指向double
类型的指针参数,并返回int
类型的函数。
构造 std::packaged_task
实例时,必须传入一个函数或可调用对象,该对象需要能够接收指定的参数并返回(可转换为指定返回类型)的值。类型可以不完全匹配,因为允许隐式类型转换。例如,可以用一个接受 int
参数并返回 float
值的函数来构造 std::packaged_task<double(double)>
。
函数签名的返回类型决定了通过 get_future()
返回的 std::future
类型,而函数签名的参数列表则用于指定 std::packaged_task
的函数调用操作符。
示例代码
以下代码展示了 std::packaged_task
的模板偏特化:
template<>
class packaged_task<std::string(std::vector<char>*, int)> {
public:
template<typename Callable>
explicit packaged_task(Callable&& f);
std::future<std::string> get_future();
void operator()(std::vector<char>*, int);
};
std::packaged_task
的特性
-
可调用性:
std::packaged_task
是一个可调用对象,可以封装在std::function
对象中,从而作为线程函数传递给std::thread
或作为可调用对象传递给其他函数。
-
异步结果存储:
- 当
std::packaged_task
被调用时,实参会传递给底层函数,其返回值作为异步结果存储在std::future
中,可通过get_future()
获取。
- 当
-
任务打包与结果取回:
- 可以使用
std::packaged_task
对任务进行打包,并在适当时候取回std::future
。当需要等待异步任务完成时,可以通过等待std::future
的状态变为“就绪”来实现。
- 可以使用
线程间传递任务:图形界面线程示例
许多图形架构要求特定的线程更新界面。因此,当某个线程需要更新界面时,可以使用 std::packaged_task
将任务发送给正确的线程,而无需发送自定义消息。
示例代码
#include <deque>
#include <mutex>
#include <future>
#include <thread>
#include <utility>
std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread() {
// 图形界面线程
while (!gui_shutdown_message_received()) {
// 循环直到收到关闭图形界面的消息
get_and_process_gui_message(