C++11学习笔记-----获取异步操作执行结果

在多线程环境中,不管是传递lambda还是传递函数指针,再或者是传递函数对象给std::thread,都很难获取执行函数返回值。在以前,只能将结果以引用的形式作为线程函数参数的一部分以此保存返回值,但是仍然存在很大局限性,甚至不太美观。C++11引入的std::future可以有效解决这一问题。

std::future定义在头文件中,提供了一种获取异步操作返回值的机制,不过通常与下列三个配合使用

  • std::promise
  • std::packaged_task
  • std::async

这三个操作各有不同,但是都有一个共同点就是都提供了get_future接口用于获得与之关联的future,使用者(主线程)可以通过返回的future获得异步操作结果。

std::promise

简单来说,promise是一种用于消息传递的机制,或者说是提供存储值和异常的设施。当创建线程时可以将promise引用传给线程函数,当在线程函数(异步操作)中计算得知了主线程想要的结果后通过promise::set_value*等接口设置值(如果出现异常也可以设置异常)。而主线程可以通过从promise获取的future获取结果

示例:利用std::future和std::promise实现并发std::find函数

和并发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

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::packaged_task实现向线程池中添加任务

在介绍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::async用于异步执行给定函数,并返回用于获取函数返回值的std::future实例。所以std::async本质上应该是开启一个线程执行给定函数,内部采用std::packaged_task对函数进行包装,然后返回std::future

std::async构造函数有一个异步属性,分别是

  • std::launch::async,表示立即开启异步求值
  • std::launch::deferred,延迟开启,只有当返回的future实例调用get函数时才开启异步求值

而默认情况下的异步属性是std::launch::async | std::launch::deferred,所以到底是立即开启还是延迟开启取决于编译器的不同。如果异步至关重要的话记得在构造函数中指定std::launch::async

示例:利用std::async实行并行std::for_each函数

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实例

你可能感兴趣的:(C++)