(七)异步编程
异步编程(Asynchronous Programming是一种编程模型,它允许程序在不阻塞当前线程的情况下启动耗时操作(如I/O操作、网络请求等),并在这些操作完成时通知当前线程,反馈结果。这种模型使得程序在等待操作完成期间能够继续执行其他任务,从而提高了计算机资源的利用率。
异步编程可以看作是并发编程的一种策略或技术。 C++ 标准库中的std::future
, std::async
, std::packaged_task
和 std::promise
用于支持异步编程。
std::future
是一个模板类,用于表示异步操作的结果。 std::future
对象通常不是直接创建的,而是与 std::async
, std::packaged_task
和 std::promise
配合使用。
std::async
std::async
是一个函数模板,它用于启动一个异步任务。这个函数接受一个可调用的对象(如函数、Lambda 表达式、函数对象等)作为参数,并在一个单独的线程上异步执行它。std::async
自动启动并管理异步任务,通过指定的执行策略(std::launch::async
强制在单独的线程上异步执行函数;std::launch::deferred
延迟执行,直到调用 std::future::get()
或 std::future::wait()
时才执行函数)返回一个 std::future
对象。
下面展示如何使用 std::future
和 std::async
来异步计算一个数的平方:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
// 计算平方的函数
int square(int x) {
auto start = std::chrono::system_clock::now();
// 为了模拟一个耗时的操作,我们让线程休眠一段时间
std::this_thread::sleep_for(std::chrono::seconds(2));
auto end = std::chrono::system_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Square computation took " << diff.count() << " seconds.\n";
return x * x;
}
int main() {
auto start_main = std::chrono::system_clock::now();
// 使用 std::async 启动一个异步任务,并传递 square 函数和参数 5
std::future<int> result = std::async(std::launch::async, square, 5);
std::cout << "Square computation is in progress...\n";
// 模拟主线程执行其他任务
std::this_thread::sleep_for(std::chrono::seconds(1));
auto after_other_task = std::chrono::system_clock::now();
std::chrono::duration<double> diff_other_task = after_other_task - start_main;
std::cout << "Main thread did something else after " << diff_other_task.count() << " seconds.\n";
// 调用 get() 获取square 函数的结果
// 注意:调用 get() 函数会阻塞当前线程,直到结果可用
int value = result.get();
// 输出结果和获取结果所用的时间
auto end_main = std::chrono::system_clock::now();
std::chrono::duration<double> diff_result = end_main - start_main;
std::cout << "The square of 5 is " << value << ". Got result after " << diff_result.count() << " seconds.\n";
return 0;
}
Square computation is in progress...
Main thread did something else after 1.01517 seconds.
Square computation took 2.0062 seconds.
The square of 5 is 25. Got result after 2.00861 seconds.
在这个示例中,异步任务(计算5的平方)需要2秒钟来完成。然而,主线程在启动异步任务后立即继续执行,并在1秒钟后输出“Main thread is doing something else…”。这表明主线程在等待异步任务完成期间没有被阻塞,而是继续执行了其他任务。最后,当主线程需要异步任务的结果时,它调用result.get()
,这时如果异步任务还没有完成,主线程将会被阻塞,直到结果可用。
std::packaged_task
std::packaged_task
:一个类模板,用于将可调用对象(如函数、lambda 表达式或函数对象)包装成任务,然后可将其传递给线程异步地执行。
调用std::packaged_task::get_future()
都会得到一个与之关联的std::future
对象,这个对象可以通过.get()
来获取任务完成后产生的结果。如果任务尚未完成,则获取结果的操作将阻塞调用线程,直到结果可用。
以下代码示例使用 std::packaged_task
实现与上小节中std::async
相同的功能 :
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
// 计算平方的函数
int square(int x) {
auto start = std::chrono::system_clock::now();
// 为了模拟一个耗时的操作,我们让线程休眠一段时间
std::this_thread::sleep_for(std::chrono::seconds(2));
auto end = std::chrono::system_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Square computation took " << diff.count() << " seconds.\n";
return x * x;
}
int main() {
auto start_main = std::chrono::system_clock::now();
// 使用 std::packaged_task 包装 square 函数
std::packaged_task<int(int)> task(square);
// 从 packaged_task 获取 future 对象
std::future<int> result = task.get_future();
// 在另一个线程中运行 task
std::thread(std::move(task), 5).detach();
std::cout << "Square computation is in progress...\n";
// 模拟主线程执行其他任务
std::this_thread::sleep_for(std::chrono::seconds(1));
auto after_other_task = std::chrono::system_clock::now();
std::chrono::duration<double> diff_other_task = after_other_task - start_main;
std::cout << "Main thread did something else after " << diff_other_task.count() << " seconds.\n";
// 调用 get() 获取 square 函数的结果
// 注意:调用 get() 函数会阻塞当前线程,直到结果可用
int value = result.get();
// 输出结果和获取结果所用的时间
auto end_main = std::chrono::system_clock::now();
std::chrono::duration<double> diff_result = end_main - start_main;
std::cout << "The square of 5 is " << value << ". Got result after " << diff_result.count() << " seconds.\n";
return 0;
}
相比于 std::async
,std::packaged_task
将异步任务的创建和执行分离开来,提供了更高的自由度。使用 std::packaged_task
,你可以显式地指定在哪个线程上执行任务,也可以更灵活地控制任务的生命周期。然而,这也意味着 std::packaged_task
的使用相对复杂一些,因为它需要更多的手动管理。
std::promise
std::promise
是一个类模板,它提供了在线程间传递值或异常的机制。通过这个机制,一个线程可以在任务执行的任意时刻使用 std::promise
对象来设置值或异常,而另一个线程则可以通过与 std::promise
对象相关联的 std::future
对象来检索这个值或异常。
示例:
#include <iostream>
#include <future>
#include <thread>
// 计算函数,接受一个promise对象和两个整数作为参数
void calculate(std::promise<int> prom, int x, int y) {
try {
// 检查除数是否为0
if (y == 0) {
// 如果除数为0,则抛出运行时错误
throw std::runtime_error("Division by zero");
}
// 否则,将计算结果设置到promise对象中
prom.set_value(x / y);
} catch (...) {
// 捕获所有异常,并将当前异常设置到promise对象中
prom.set_exception(std::current_exception());
}
}
int main() {
// 创建一个promise对象,用于存储异步操作的结果或异常
std::promise<int> prom;
// 从promise对象中获取一个future对象,用于获取异步操作的结果或异常
std::future<int> result = prom.get_future();
// 启动一个新线程来执行calculate函数,并将promise对象(通过移动语义传递)和两个整数作为参数
std::thread t(calculate, std::move(prom), 10,0);
// 调用result.get()来获取异步操作的结果,如果操作抛出异常,则get()会重新抛出该异常
try {
std::cout << "Result: " << result.get() << std::endl; // 如果操作成功,输出计算结果
} catch (const std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl; // 如果操作抛出异常,输出异常信息
}
t.join();
return 0;
}
Result: Exception: Division by zero