获得线程执行任务的结果
在 C++ 11 之前,想要从线程返回执行任务的结果,可以通过指针来完成。
void fun(int x, int y, int* ans, std::condition_variable &cv) {// 模拟求值之前的准备工作this_thread::sleep_for(3s);*ans = x + y;cv.notify_one();
}int main()
{int a = 10;int b = 8;int sum = 0;std::mutex m;std::condition_variable cv;std::thread t(fun, a, b, &sum, std::ref(cv));// 模拟主线程中的其他操作this_thread::sleep_for(1s);unique_lock<mutex> l{m};cv.wait(l);std::cout << sum << std::endl; // 输出:18t.join();return 0;
}
可以看到,要通过指针来传递结果,在操作上比较复杂需要涉及到 mutex
、unique_lock
、condition_variable
,且逻辑上没有那么舒服。因此 C++ 提供了 std::future
类模板。
std::future
C++11 提供了 std::future
类模板,future
对象提供访问异步操作结果的机制,很轻松解决从异步任务中返回结果。
事实上,一个 std::future
对象在内部存储一个将来会被某个 provider 赋值的值,并提供了一个访问该值的机制,通过get()
成员函数实现。但如果有人试图在值可用之前通过get()
来访问相关的值,那么get()
函数将会阻塞,直到该值可用。
逻辑上来说 std::future 是“一次性事件”,也就是说这个事件只会发生一次,发生之后就会被销毁无法再次使用。既然是一个事件,在时间跨度上来说,必然存在两种状态:即事件发生前和事件发生后。对应的 std::future 也存在两种状态,即未就绪状态和就绪状态,当std::future处于非就绪状态时,就表示执行的任务还没有得出结果,此时调用get(),调用线程会等待任务线程执行完成;当std::future处于就绪状态时,就表示执行的任务已经得出结果,可以通过 get() 立即取得值。既然前文提到了 std::future 表示的是一次性事件,“一次性”是什么意思?“一次性”表示只能调用 get() 方法一次,以后就不能再调用 get() 了。要知道 std::future 是否调用过 get() ,可以通过成员函数 vaild() 。
一个有效的std::future
对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,他们分别是:
std::async
std::promise::get_future
std::packaged_task::get_future
std::async
对于简单的我们不需要获得结果的任务(即没有返回值的可调用对象)来说,使用 std::thread
可以轻松地完成。然而当我们需要任务的结果,std::thread
并不能满足要求,我们需要求助于函数模板 std::async
。std::async
在具有 std::thread
功能的基础上提供了返回值 std::future
(这是一个一次性任务)。通过 std::future
可以获得任务的结果(调用 std::future
)。下面给一个小 demo:
int sayHello(){cout << "hello world" << endl;return 1;
}int main() {std::future<int> future = std::async(sayHello);cout << future.get() << endl;// cout << future.get() << endl; // 上文提到了一次性任务,简单理解就是只能调用一次 get() 方法获得任务的结果,// 当第二次调用 get() 时就会报错,因为这已经被使用过一次。return 0;
}
使用 std::async
并通过 std::future
获得任务结果并不是唯一将任务与结果关联的方式,使用 std::packaged_task
可以更好得完成这项操作,同时其也可作为线程池的基本构件。 本质上来说 std::packaged_task
连结了 std::future
对象与可调用对象,将模板函数 std::async
进一步抽象成了一个模板类从而方便使用。
std::packaged_task
下面是 std::packaged_task
的部分类定义,可以了解到其模板参数是函数签名。
template<typename _Res, typename... _ArgTypes>class packaged_task<_Res(_ArgTypes...)>
前文提到 std::packaged_task
连结了可调用对象,其本身也是个可调用对象,我们可以直接调用,还可以将其包装在 std::function
对象内,当做入参传递给另一个线程 std::thread
来调用,也可以给任何需要可调用对象的函数。std::packaged_task
对象作为函数对象在被调用的过程中, 会通过函数调用操作符接受参数,并将其进一步传递给包装的任务函数,由其运行得出结果,并将结果保存到关联的 std::future
对象内部。对于外部来说可以通过调用 std::packaged_task
的成员函数 get_future()
来获得关联的 std::future
。下面给出小的使用demo:
int sayHello(){cout << "hello world" << endl;return 1;
}
int main() {std::packaged_task<int()> task{sayHello};std::thread t1{[&](){task();}};std::future<int> future = task.get_future();t1.join();cout << future.get() << endl;return 0;
}
这里需要注意的是 std::packaged_task
删除了拷贝构造和拷贝赋值函数(因此在传递的时候需要用到 std::move
)。
std::promise
和 std::packaged_task
关联了一个 std::future
一样,std::promise
也关联了一个 std::future
,可以通过其成员函数 get_future()
获得。相比于通过 std::packaged_task
必须关联一个可调用对象才能用,std::promise
的使用自由度更高。可以将一对 std::promise
和 std::future
对象分别传给不同线程,来让两个线程传值。赋值线程通过调用 std::promise
的成员函数 set_value()
方法可以设置值, 需要值的线程通过调用 std::future
的 get()
来取得值。
void fun(int x, int y, std::promise<int>& promiseObj) {promiseObj.set_value(x + y);
}int main()
{int a = 10;int b = 8;std::promise<int> promiseObj;std::future<int> futureObj = promiseObj.get_future();std::thread t(fun, a, b, std::ref(promiseObj));t.join();int sum = futureObj.get();std::cout << "sum=" << sum << std::endl; // 输出:18return 0;
}
在任务线程中抛出异常该如何被调用线程获取
在生产环境中的程序往往会遇到各种异常,比如,硬盘写满、网络故障、数据库崩溃等。但是当任务线程抛出异常时,调用线程该如何知道任务线程抛出了异常而不是任务结果。想想这是一个非常难处理的问题,幸运的是 std::future 会保存抛出的异常。经由 std::sync() 调用的函数或者包装了任务函数的 std::packaged_task 在执行任务抛出异常值时,异常会代替本该被设定的值被保存在 future 中。 当在调用线程中调用 std::future 成员函数 get() 时,异常被重新在调用线程中抛出抛出。下面是一个小demo:
double square_root(double x){if(x < 0){throw std::out_of_range(" x < 0"); // 异常被保存在 future 上}return sqrt(x);
}int main() {std::future<double> f = std::async(square_root, -1); // 在子线程中运行 square_root 任务double y = f.get(); // 在调用 get() 方法时重新抛出异常return 0;
}
参考
【C++11 多线程】future与promise(八)
C++ 并发编程实战(第二版)