概念
我们前面介绍的std::thread 是C++11中提供异步创建多线程的工具,只能是异步运行任务,却无法获取任务执行的结果,一般都是依靠全局对象,全局对象在多线程下是及其不安全的,为此标准库提供了std::future 类模板来关联线程运行的函数和函数的返回结果,这种获取结果的方式是异步的。
std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。
一个有效(valid)的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,他们分别是:
- std::async 函数
- std::promise::get_future,get_future 为 promise 类的成员函数
- std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数
一个 std::future 对象只有在有效(valid)的情况下才有用(useful),由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)。
std::future接口
template<typename ResultType>
class future
{
public:
future() noexcept;
future(future&&) noexcept;
future& operator=(future&&) noexcept;
~future();
future(future const&) = delete;
future& operator=(future const&) = delete;
bool valid() const noexcept;
ResultType get();
shared_future<ResultType> share();
void wait();
template<typename Rep,typename Period>
future_status wait_for(
std::chrono::duration<Rep,Period> const& relative_time);
template<typename Clock,typename Duration>
future_status wait_until(
std::chrono::time_point<Clock,Duration> const& absolute_time);
};
-
构造函数:
- 不带参数的默认构造函数,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它;
- 禁用拷贝构造;
- 支持移动构造。
std::future<int> fut; // 默认构造函数
fut = std::async(do_some_task); // move-赋值操作。
-
析构函数:
-
get函数:
- 当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。
- 如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
- 当共享状态就绪后,则该函数将取消阻塞并返回(或抛出)释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。
std::future<void>::get() (void特例化)不返回任何值,但仍等待共享状态就绪并释放它。- 共享状态是作为原子操作(atomic operation)被访问。
#include <iostream> // std::cout
#include <future> // std::async, std::future
#include <utility> // std::move
int do_get_value() { return 11; }
int main () {
// 由默认构造函数创建的 std::future 对象,
// 初始化时该 std::future 对象处于为 invalid 状态.
std::future<int> foo, bar;
foo = std::async(do_get_value); // move 赋值, foo 变为 valid.
bar = std::move(foo); // move 赋值, bar 变为 valid, 而 move 赋值以后 foo 变为 invalid.
if (foo.valid()) {
std::cout << "foo's value: " << foo.get() << '\n';
} else {
std::cout << "foo is not valid\n";
}
if (bar.valid()) {
std::cout << "bar's value: " << bar.get() << '\n';
} else {
std::cout << "bar is not valid\n";
}
return 0;
}
-
operator=:
- 禁用拷贝赋值。
- 支持移动赋值:如果在调用之前,此对象是有效的(即它已经访问共享状态),则将其与先前已关联的共享状态解除关联。如果它是与先前共享状态关联的唯一对象,则先前的共享状态也会被销毁。
-
share函数:
- 获取共享的future,返回一个
std::shared_future 对象,该对象获取future对象的共享状态。调用该函数之后,该 std::future 对象本身已经不和任何共享状态相关联,因此该std::future 的状态不再是 valid 的了。 -
valid函数:
- 检查共享状态的有效性,返回当前的
future 对象是否与共享状态关联。一个有效的std::future 对象只能通过 std::async() , std::future::get_future 或者 std::packaged_task::get_future 来初始化。另外由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。一旦调用了std::future::get() 函数,再调用此函数将返回false。 -
wait函数:
- 等待共享状态就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
- 当共享状态就绪后,则该函数将取消阻塞并返回。但是
wait() 并不读取共享状态的值或者异常。 -
wait_for函数:
- 等待共享状态在指定的时间内(time span)准备就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪或已达到设置的时间。
- 此函数的返回值类型为枚举类
future_status 。此枚举类有三种label:
- ready:共享状态已就绪;
- timeout:在指定的时间内未就绪;
- deferred:共享状态包含了一个延迟函数(deferred function)。
-
wait_until函数:
- 等待共享状态在指定的时间点(time point)准备就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪或已达到指定的时间点。
- 此函数的返回值类型为枚举类
future_status 。
下面来看看详细的代码:
#include <iostream>
#include <future>
#include <chrono>
#include <utility>
#include <thread>
int test_future_1()
{
{ // constructor/get/operator=
auto get_value = []() { return 10; };
std::future<int> foo; // default-constructed
// move-constructed
std::future<int> bar = std::async(get_value);
int x = bar.get();
std::cout << "value: " << x << '\n'; // 10
std::future<int> foo2(std::async(get_value));
std::cout << "value: " << foo2.get() << '\n'; // 10
}
{ // share
std::future<int> fut = std::async([]() { return 10; });
std::shared_future<int> shfut = fut.share();
//std::cout << "value: " << fut.get() << '\n';
// crash, 执行完fut.share()后,fut对象将变得无效
std::cout << "fut valid: " << fut.valid() << '\n';// 0
// shared futures can be accessed multiple times:
std::cout << "value: " << shfut.get() << '\n'; // 10
// 20, 对于std::shared_future对象,get函数可以被多次访问
std::cout << "its double: " << shfut.get() * 2 << '\n';
}
{ // valid
std::future<int> foo, bar;
foo = std::async([]() { return 10; });
bar = std::move(foo);
if (foo.valid()) {std::cout << "foo's value: " << foo.get() << '\n';}
else {std::cout << "foo is not valid\n";}
if (bar.valid()) {std::cout << "bar's value: " << bar.get() << '\n';}
else {std::cout << "bar is not valid\n";}
}
{ // wait
auto is_prime = [](int x) {
for (int i = 2; i < x; ++i) if (x%i == 0) return false;
return true;
};
// call function asynchronously:
std::future<bool> fut = std::async(is_prime, 194232491);
std::cout << "checking...\n";
fut.wait();
std::cout << "\n194232491 ";
// guaranteed to be ready (and not block) after wait returns
if (fut.get()) {
std::cout << "is prime.\n";
} else {
std::cout << "is not prime.\n";
}
}
{ // wait_for
auto is_prime = [](int x) {
for (int i = 2; i < x; ++i) if (x%i == 0) return false;
return true;
};
// call function asynchronously:
std::future<bool> fut = std::async(is_prime, 700020007);
// do something while waiting for function to set future:
std::cout << "checking, please wait";
std::chrono::milliseconds span(100);
// 可能多次调用std::future::wait_for函数
while (fut.wait_for(span) == std::future_status::timeout)
std::cout << '.';
bool x = fut.get(); // retrieve return value
std::cout << "\n700020007 " << (x ? "is" : "is not") << " prime.\n";
}
return 0;
}
int test_future_2()
{
// future from a packaged_task
std::packaged_task<int()> task([] { return 7; }); // wrap the function
std::future<int> f1 = task.get_future(); // get a future
std::thread t(std::move(task)); // launch on a thread
// future from an async()
std::future<int> f2 = std::async(std::launch::async, [] { return 8; });
std::cout << "Waiting..." << std::flush;
f1.wait();
f2.wait();
std::cout << "Done!\nResults are: " << f1.get()
<< ' ' << f2.get() << ' ' << '\n';
t.join();
return 0;
}
void initiazer(std::promise<int> * promObj)
{
std::cout << "Inside Thread" << std::endl;
promObj->set_value(35);
}
int test_future_3()
{
std::promise<int> promiseObj;
std::future<int> futureObj = promiseObj.get_future();
std::thread th(initiazer, &promiseObj);
std::cout << "value: " << futureObj.get() << std::endl;
th.join();
// If std::promise object is destroyed before setting the value
// the calling get() function on associated std::future object will throw exception.
// A part from this, if you want your thread to return multiple values
// at different point of time then
// just pass multiple std::promise objects in thread
// and fetch multiple return values from thier associated multiple std::future objects.
return 0;
}
int main() {
test_future_1();
test_future_2();
test_future_3();
}
通过std::async() 创建异步任务的std::future ,std::async 的创建任务的传参方式和 std::thread 一样,也可以使用类的成员方法进行创建:
#include <future>
#include <iostream>
class A {
public:
int f(int i) { return i; }
};
int main() {
A a;
std::future<int> res = std::async(&A::f, &a, 1);
std::cout << res.get(); // 1,阻塞至线程返回结果
}
需要注意,std::future 只能 get() 一次,多次get会抛出异常:
#include <future>
#include <iostream>
int main() {
std::future<void> res = std::async([] {});
res.get();
try {
res.get();
} catch (const std::future_error& e) {
// exception: std::future_error: No associated state
std::cout << e.what() << std::endl;
}
}
std::shared_future
std::shared_future 类型模板是为了等待其他线程上的异步结果。其和std::promise ,std::packaged_task 类型模板,还有std::async 函数模板,都是为异步结果准备的工具。与std::future 唯一的区别就是多个std::shared_future 实例可以引用同一个异步结果。
std::shared_future 实例是CopyConstructible (拷贝构造)和CopyAssignable (拷贝赋值)。你也可以用ResultType 的std::future 类型对象,移动构造一个std::shared_future 类型对象。
访问给定std::shared_future 实例是非同步的。因此,当有多个线程访问同一个std::shared_future 实例,且无任何外围同步操作时,这样的访问是不安全的。不过访问关联状态时是同步的,所以多个线程访问多个独立的std::shared_future 实例,且没有外围同步操作的时候,是安全的。
shared_future的接口与future基本一致,这里就不再详细介绍了:
template<typename ResultType>
class shared_future
{
public:
shared_future() noexcept;
shared_future(future<ResultType>&&) noexcept;
shared_future(shared_future&&) noexcept;
shared_future(shared_future const&);
shared_future& operator=(shared_future const&);
shared_future& operator=(shared_future&&) noexcept;
~shared_future();
bool valid() const noexcept;
ResultType get() const;
void wait() const;
template<typename Rep,typename Period>
future_status wait_for(
std::chrono::duration<Rep,Period> const& relative_time) const;
template<typename Clock,typename Duration>
future_status wait_until(
std::chrono::time_point<Clock,Duration> const& absolute_time)
const;
};
std::shared_future 可以多次获取结果,它可以通过std::future 的右值构造。每一个std::shared_future 对象上返回的结果不同步,多线程访问std::shared_future 需要加锁防止 race condition,更好的方法是给每个线程拷贝一个 std::shared_future 对象,这样就可以安全访问而无需加锁:
#include <iostream>
#include <future>
int main() {
std::promise<int> ps;
std::future<int> ft = ps.get_future();
std::shared_future<int> sf(std::move(ft));
// 或直接 std::shared_future<int> sf{ps.get_future()};
ps.set_value(1);
int ret = sf.get();
std::cout << "get1: "<< ret << std::endl;
ret = sf.get(); //std::shared_future可以多次get
std::cout << "get2: "<< ret << std::endl;
}
输出:
get1: 1
get2: 1
也可以直接用std::future::share() 生成std::shared_future :
#include <iostream>
#include <future>
int main() {
std::promise<int> ps;
auto sf = ps.get_future().share();
ps.set_value(2);
int ret = sf.get();
std::cout << "get1: "<< ret << std::endl;
ret = sf.get();
std::cout << "get2: "<< ret << std::endl;
}
输出:
get1: 2
get2: 2
实现原理猜想
其实写这个有点超出本人目前的能力范围了,因为还没能力去读它的源码。但是还是想在这里猜想一下,从当前的这个工作场景来其实很像很像我们之前介绍过的条件变量:C++多线程:condition_variable - 掘金 (juejin.cn)。我们不防猜想一下:
-
情况1:可以使用条件变量实现,future对象中设置一个条件变量,在异步线程结束时调用notify_one(),在get()函数中调用wait(),这样可以实现一个简单的异步调用,缺点是需要互斥量,条件变量,一个仅发生一次的过程这样不免有些浪费,wait()操作更是需要加锁解锁,也就是说这样一个完整的过程我们需要加锁解锁各两次,还需要一个notify_one(),但优点也很明显,过程简单,且如果等待时间较长,可以把cpu让给其他工作线程,全局上节省的时间随等待时间加长而变长,但等待时间短的话除了完成功能就没有丝毫优势了。 -
情况2:也可以使用自旋锁实现,自旋锁(spinlock),是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。future对象只需要设置一个类内的原子变量,当异步线程结束后改变值,然后get()成员自旋等待即可,这种方法优点与缺点都是很明显的,优点:比起条件变量这样笨重的东西确实轻盈了不少,且在等待时间较小时不存在条件变量所需要的用户态与内核态之间的转换;缺点:获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,因此使用这种锁会造成busy-waiting,也就是CPU不断的进行检测操作,而无法处理其他任务。当等待时间较长的时候cpu空转,无意义的消耗,当然这是自旋锁本身的弊端。
最后查阅了一些资料证实了future最终是使用了情况2-自旋锁实现,但是与标准的自旋锁有一些差异,具体的细节以后有能力了再详细介绍了。
参考
C++11 并发指南四( 详解三 std::future & std::shared_future) - Haippy - 博客园 (cnblogs.com)
|