C++11学习笔记-----获取异步操作执行结果
在多線程環境中,不管是傳遞lambda還是傳遞函數指針,再或者是傳遞函數對象給std::thread,都很難獲取執行函數返回值。在以前,只能將結果以引用的形式作為線程函數參數的一部分以此保存返回值,但是仍然存在很大局限性,甚至不太美觀。C++11引入的std::future可以有效解決這一問題。
std::future定義在頭文件<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 <future> #include <thread> #include <vector> #include <algorithm> #include <cassert>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<InputIt> 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);elseback = 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<F(Args...)>::type> {/* 獲取函數f的返回結果,因為std::future模板參數需要保存結果類型 */using return_type = typename std::result_of<F(Args...)>::type;/* std::packaged_task不允許復制,所以用指針保存 *//* std::bind()返回可調用對象,包裝在packaged_task中 */auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));/* 獲取future,用于獲得執行結果 */std::future<return_type> 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<InputIt, UnaryFunction>, middle, last, f);/* 當前線程執行前半部分 */tinystl::for_each(first, middle, f);}return f; }小結
std::future提供了獲取異步操作執行結果的機制,std::promise用于保存值和異常,可以看成是消息傳遞的一種,std::packaged_task用于對可調用對象的保證,std::async會開啟一個異步操作,效果等同于創建新線程(或將執行函數添加到線程池),包裝線程函數,返回future實例
總結
以上是生活随笔為你收集整理的C++11学习笔记-----获取异步操作执行结果的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每天一道LeetCode-----有序数
- 下一篇: C++代码片段(一)萃取函数返回值类型,