在多线程环境中,不管是传递lambda还是传递函数指针,再或者是传递函数对象给std::thread,都很难获取执行函数返回值。在以前,只能将结果以引用的形式作为线程函数参数的一部分以此保存返回值,但是仍然存在很大局限性,甚至不太美观。C++11引入的std::future可以有效解决这一问题。
std::future定义在头文件
这三个操作各有不同,但是都有一个共同点就是都提供了get_future接口用于获得与之关联的future,使用者(主线程)可以通过返回的future获得异步操作结果。
简单来说,promise是一种用于消息传递的机制,或者说是提供存储值和异常的设施。当创建线程时可以将promise引用传给线程函数,当在线程函数(异步操作)中计算得知了主线程想要的结果后通过promise::set_value*等接口设置值(如果出现异常也可以设置异常)。而主线程可以通过从promise获取的future获取结果
和并发std::accumulate的实现类似,首先计算合适的线程数,将给定区间拆分成若干小区间,并行执行查找操作,当找到结果后,通过std::promise设置查找结果,而主线程则通过std::future获取结果
#include
#include
#include
#include
#include
namespace parallel
{
template <class InputIt, class T>
InputIt find(InputIt first, InputIt last, const T& value)
{
/*
* 计算合适的线程数
* std::thread::hardware_concurrency()用于返回当前系统支持的并发数
*/
auto count = std::distance(first, last);
auto avaThreadNums = std::thread::hardware_concurrency();
auto perThreadMinNums = 20;
auto maxThreadNums = ((count + (perThreadMinNums - 1)) & (~(perThreadMinNums - 1))) / perThreadMinNums;
auto threadNums =
avaThreadNums == 0 ?
maxThreadNums :
std::min(static_cast<int>(maxThreadNums), static_cast<int>(avaThreadNums));
auto blockSize = count / threadNums;
/* 主线程创建std::promise实例,模板参数是返回值类型 */
std::promise result;
/* 因为不同线程会并发查找,当一个线程找到后其他线程就可以停止查找了,原子变量done用于标记是否找到 */
std::atomic<bool> done(false);
{
std::vector<std::thread> threads;
auto front = first;
for(int i = 0; i < threadNums; ++i)
{
auto back = front;
if(i != threadNums - 1)
std::advance(back, blockSize);
else
back = last;
threads.emplace_back(
[front, back, &value, &result, &done]
{
/* 当一个线程找到后所有线程都会退出,通过done标记管理 */
for(auto it = front; !done && it != back; ++it)
{
if(*it == value)
{
done.store(true);
/* 如果找到,记录找到的值 */
result.set_value(it);
return;
}
}
}
);
}
/* 回收线程资源 */
for(auto &th : threads)
th.join();
}
/* 通过std::promise::get_future获得std::future对象,然后调用get获取结果 */
return done ? result.get_future().get() : last;
}
}
int main()
{
std::vector<int> v(100000000);
int n = 0;
std::generate(v.begin(), v.end(), [&n] { return ++n; });
auto value = std::random_device()() % 65536;
auto it1 = parallel::find(v.begin(), v.end(), value);
auto it2 = std::find(v.begin(), v.end(), value);
assert(it1 == it2);
return 0;
}
本例中同时并发了多个线程执行find操作,而最后只需要获取找到结果的那个线程返回的值,不管哪个线程找到结果,都可以记录在std::promise实例中,最终通过std::future返回
当然,使用std::promise的做法和给线程函数传入引用记录结果的做法基本相同,不过std::promise的功能不仅仅局限于此,使用起来也更加容易,结构更加清晰
std::packaged_task用于包装任何可调用对象,无非就是函数指针,函数对象,lambda等,功能类似于std::function,但是packaged_task可以通过返回的future获取异步操作的结果。
举个例子,当存在一个函数,而这个函数通常会被其它线程执行时,那么想要获取这个函数的返回值就是件困难的事情,以std::function为例,假设在一个线程池中,主线程通过std::function包装了一个函数,添加到任务队列中,随后线程池中其它线程取出这个任务函数并开始执行,在这种情况下,主线程是很难获取这个函数的返回值的。换做std::packaged_task就不同了,它可以通过get_future接口获取std::future实例,正如先前所说,std::future用于获取异步操作的结果,所以无论函数由谁执行,都可以通过std::future::get接口获取返回值
在介绍std::thread的那一篇中,涉及到了线程池的实现,借着对std::packaged_task的理解,重新实现一下向任务队列中添加任务的函数,同时需要确保调用者能够获取任务函数返回的结果,这里可以返回给调用者一个std::future实例。另外,获取std::future实例有三种方法,其中涉及到函数包装的是std::packaged_task,所以在添加任务时,将任务函数包装在packaged_task中,返回future
template <class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args... args)
-> std::future<typename std::result_of::type>
{
/* 获取函数f的返回结果,因为std::future模板参数需要保存结果类型 */
using return_type = typename std::result_of::type;
/* std::packaged_task不允许复制,所以用指针保存 */
/* std::bind()返回可调用对象,包装在packaged_task中 */
auto task = std::make_shared<std::packaged_task>(
std::bind(std::forward(f), std::forward(args)...)
);
/* 获取future,用于获得执行结果 */
std::future result = task->get_future();
{
std::unique_lock<std::mutex> lock(mutex_);
tasks_.push([task] { (*task)(); });
cond_.notify_one();
}
/* 返回future */
return result;
}
int main()
{
ThreadPool pool(4);
std::vector<std::future<int>> results;
for(int i = 0; i < 10; ++i)
{
results.emplace_back(
pool.enqueue(
[i]
{
return i * i;
}
)
);
}
for(auto&& result : results)
std::cout << result.get() << std::endl;
return 0;
}
对于std::async而言,感觉它的抽象要深一些,std::async用于异步执行给定函数,并返回用于获取函数返回值的std::future实例。所以std::async本质上应该是开启一个线程执行给定函数,内部采用std::packaged_task对函数进行包装,然后返回std::future
std::async构造函数有一个异步属性,分别是
而默认情况下的异步属性是std::launch::async | std::launch::deferred,所以到底是立即开启还是延迟开启取决于编译器的不同。如果异步至关重要的话记得在构造函数中指定std::launch::async
std::for_each会对指定区间的每一个元素执行给定的函数,所以完全可以并行化。
template <class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
auto count = tinystl::distance(first, last);
if(!count) return f;
if(count <= 100)
{
tinystl::for_each(first, last, f);
}
else
{
auto middle = first;
tinystl::advance(middle, count / 2);
/* 开启异步操作对后半部分执行for_each */
std::async(std::launch::async, tinystl::parallel::for_each, middle, last, f);
/* 当前线程执行前半部分 */
tinystl::for_each(first, middle, f);
}
return f;
}
std::future提供了获取异步操作执行结果的机制,std::promise用于保存值和异常,可以看成是消息传递的一种,std::packaged_task用于对可调用对象的保证,std::async会开启一个异步操作,效果等同于创建新线程(或将执行函数添加到线程池),包装线程函数,返回future实例