? ? ? ? std::promise、future、thread、async、packaged_task这些到底是个啥?
????????std::future是一个同步原语,它代表了一个异步操作的结果。这个结果可能来自另一个线程、任务或者异步操作,而std::future提供了一种机制来访问这个结果。std::future通常与std::async、std::promise或std::packaged_task一起使用。
????????std::future对象封装了一个值,这个值在将来某个时刻才会变得可用。当异步操作完成时,它的结果(或异常)会存储在std::future对象中。可以通过std::future对象来检索这个结果,或者等待异步操作完成。
获取std::future
可以通过以下几种方式获取std::future对象:
使用std::future获取结果
异常处理
????????如果异步操作抛出了异常,这个异常会被捕获并存储在std::future对象中。当你调用get方法时,异常会被重新抛出。
try {
? ? int x = fut.get();
} catch (std::exception& e) {
? ? // 处理异常
}
std::future对象在默认构造时不关联任何异步操作。只有当它与一个特定的异步操作关联后,它才变得有用。std::future对象是不可复制的,但它们可以被移动。这意味着你可以将它们从一个函数传递到另一个函数,但不能创建它们的副本。
????????通过std::future,开发者可以轻松地管理异步操作的结果,无论是来自std::async启动的任务,还是std::promise和std::packaged_task的结果。std::future提供了一种安全且简单的方式来同步线程间的操作,使得编写并发和异步代码变得更加容易和安全。
????????std::promise是一个同步原语,它提供了一种在一个线程中存储一个值或异常,并在另一个线程中通过std::future对象来检索它的机制。std::promise和std::future通常一起使用,以在线程间传递数据或通知。
创建std::promise
#include <chrono>
#include <thread>
#include <future>
using namespace std;
void SlowAdd(int a, int b, promise<int>&& p) {
this_thread::sleep_for(std::chrono::seconds(1));
p.set_value(a + b);
}
int main() {
promise<int> p;
// 与每个std::promise对象关联的是一个std::future对象
future<int> f = p.get_future();
thread t(SlowAdd, 1, 2, move(p));
cout << f.get() << endl;
t.join();
return 0;
}
输出:
code % ./a.out
3 // after 1 sec
异常处理
如果在计算过程中发生异常,你可以使用std::promise的set_exception方法来存储异常:
try {
? ? // 执行一些可能抛出异常的计算
} catch (...) {
? ? myPromise.set_exception(std::current_exception());
}
????????在另一个线程中,当你尝试通过std::future对象获取结果时,如果有异常被存储,它将被重新抛出。std::promise对象一旦通过set_value或set_exception设置了值或异常,就不能再次设置,否则将会抛出std::future_error异常。
????????此外,std::promise对象在析构时会自动释放与之关联的资源,但如果你在析构前没有设置值或异常,与之关联的std::future对象在调用get()时将抛出std::future_error异常。
????????std::thread代表了一个单独的执行线程。创建一个std::thread对象时,你可以传递一个函数或者任何可调用对象(包括lambda表达式、函数指针、成员函数指针和具有operator()的对象)给它,这个函数或对象将在新线程中执行。
创建线程
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
void Add(int a, int b) {
cout << "sum: " << a + b << endl;
}
class C_Sub {
public:
void Sub(int a, int b) {
cout << "sub: " << a - b << endl;
}
};
int main() {
thread t1(Add, 1, 2);
t1.join();
this_thread::sleep_for(std::chrono::seconds(1));
C_Sub sub;
thread t2(&C_Sub::Sub, sub, 3, 2);
t2.join();
return 0;
}
输出:
code % ./a.out
sum: 3
sub: 1
线程管理
????????创建线程后,就需要管理它的生命周期。通常,你需要在某个时刻等待线程结束,这可以通过调用join()来实现。如果你没有在std::thread对象销毁前调用join()或者detach(),或者重复调用了join()或detach(),那么在其析构函数中会调用std::terminate(),程序将异常终止。
线程与异常
????????如果线程函数抛出了一个未捕获的异常,std::terminate()将被调用,程序将终止。因此,你应该确保线程函数中的异常被妥善处理。
????????std::async是一个函数模板,它用于异步执行一个任务,并返回一个std::future对象。这个std::future对象可以用来获取异步任务的结果。std::async可以选择立即启动一个新线程来执行任务,也可以延迟执行任务直到对应的std::future对象的get()或wait()被调用。
使用std::async启动异步任务
#include <iostream>
#include <chrono>
#include <future>
using namespace std;
int SlowAdd(int a, int b) {
this_thread::sleep_for(std::chrono::seconds(1));
return a + b;
}
int main() {
future<int> f = async(SlowAdd, 1, 2);
cout << f.get() << endl;
return 0;
}
输出:
code % ./a.out
3 // after 1 sec
std::async的启动策略
????????std::async可以接受一个可选的启动策略参数,这个参数控制异步任务的执行方式。
????????如果不指定启动策略,std::async可能会根据实现选择最合适的策略。
异步任务的异常处理
????????如果异步任务抛出了异常,这个异常不会立即传播到启动任务的线程。相反,异常会被存储在std::future对象中,当你调用get()来获取结果时,异常会被重新抛出。
std::async与std::thread的比较
????????std::async与std::thread都可以用来执行并发任务,区别:
????????std::packaged_task是C++11引入的一个模板类,它封装了一个可调用对象(如函数、lambda表达式、绑定表达式或其他函数对象),并允许其异步执行。它的一个关键特性是能够提供一个std::future对象,通过这个对象可以在未来某个时刻获取std::packaged_task所封装的可调用对象的返回值。
如何使用std::packaged_task
#include <iostream>
#include <chrono>
#include <future>
using namespace std;
int SlowAdd(int a, int b) {
this_thread::sleep_for(std::chrono::seconds(1));
return a + b;
}
int main() {
? ? std::packaged_task<int(int)> task(SlowAdd);
? ? std::future<int> f = task.get_future();
? ? std::thread t(std::move(task), 1, 2);
? ? std::cout << f.get() << std::endl;
? ? t.join();
? ? return 0;
}
输出:
code % ./a.out
3 // after 1 sec
异常处理
????????如果std::packaged_task中的可调用对象在执行过程中抛出异常,这个异常将被捕获,并存储在与之关联的std::future对象中。当你调用std::future::get()时,如果有异常发生,它将被重新抛出。
重置任务
????????std::packaged_task提供了一个reset成员函数,允许你重置任务的状态,这样你就可以重新使用std::packaged_task对象。但是,在重置之前,你必须确保已经获取了之前任务的结果,否则会抛出std::future_error异常。
有async为什么还要有packaged_task
????????std::async是一个函数模板,它用于简化异步任务的创建和执行。当你调用std::async时,它会自动创建一个线程(或者将任务提交给线程池,这取决于实现和传递的策略参数),并立即开始执行指定的任务。std::async返回一个std::future对象,你可以通过它来获取任务的结果。
????????std::async的优点在于它的简单性和便捷性。你不需要显式创建线程或管理线程的生命周期,一切都由std::async自动处理。
????????std::packaged_task是一个类模板,它将一个可调用对象和一个std::future对象绑定在一起。与std::async不同,std::packaged_task不会自动启动任务执行,你需要手动调用它或将其传递给一个线程来执行。这意味着你可以更精细地控制任务的执行时机和方式。