本系列文章主要介紹 C++11 並發編程,計划分為 9 章介紹 C++11 的並發和多線程編程,分別如下:
C++11 並發指南一(C++11 多線程初探)(本章計划 1-2 篇,已完成 1 篇)
C++11 並發指南二(std::thread 詳解)(本章計划 1-2 篇,已完成 1 篇)
C++11 並發指南三(std::mutex 詳解)(本章計划 1-2 篇,已完成 2 篇)
C++11 並發指南四(future 詳解)(本章計划 3 篇,已完成 3 篇)
- C++11 並發指南四(<future> 詳解一 std::promise 介紹)
- C++11 並發指南四(<future> 詳解二 std::packaged_task 介紹)
- C++11 並發指南四(<future> 詳解三 std::future & std::shared_future)
C++11 並發指南五(std::condition_variable 詳解)(本章計划 1 篇,已完成 1 篇)
C++11 並發指南六(atomic 詳解)(本章計划 4 篇,已完成 4 篇)
- C++11 並發指南六(atomic 類型詳解一 atomic_flag 介紹)
- C++11 並發指南六( <atomic> 類型詳解二 std::atomic )
- C++11 並發指南六(atomic 類型詳解三 std::atomic (續))
- C++11 並發指南六(atomic 類型詳解四 C 風格原子操作介紹)
C++11 並發指南七(C++11 內存模型)(本章計划 3-4 篇,已完成 1 篇,3 篇在草稿中)
C++11 並發指南八(雜項)(本章計划 1-2 篇,已完成 0 篇,1 篇在草稿中)
C++11 並發指南九(綜合運用: C++11 多線程下生產者消費者模型詳解)(本章計划 1-2 篇,已完成 1 篇)
按照目前的進度來看大約完成了總體進度的 60% 左右,希望對大家理解和掌握 C++11 的並發和多線程編程有一定幫助。
上一篇《C++11 並發指南二(std::thread 詳解)》中主要講到了 std::thread 的一些用法,並給出了兩個小例子,本文將介紹 std::mutex 的用法。
Mutex 又稱互斥量,C++ 11中與 Mutex 相關的類(包括鎖類型)和函數都聲明在 <mutex> 頭文件中,所以如果你需要使用 std::mutex,就必須包含 <mutex> 頭文件。
<mutex> 頭文件介紹
Mutex 系列類(四種)
- std::mutex,最基本的 Mutex 類。
- std::recursive_mutex,遞歸 Mutex 類。
- std::time_mutex,定時 Mutex 類。
- std::recursive_timed_mutex,定時遞歸 Mutex 類。
Lock 類(兩種)
- std::lock_guard,與 Mutex RAII 相關,方便線程對互斥量上鎖。
- std::unique_lock,與 Mutex RAII 相關,方便線程對互斥量上鎖,但提供了更好的上鎖和解鎖控制。
其他類型
- std::once_flag
- std::adopt_lock_t
- std::defer_lock_t
- std::try_to_lock_t
函數
- std::try_lock,嘗試同時對多個互斥量上鎖。
- std::lock,可以同時對多個互斥量上鎖。
- std::call_once,如果多個線程需要同時調用某個函數,call_once 可以保證多個線程對該函數只調用一次。
std::mutex 介紹
下面以 std::mutex 為例介紹 C++11 中的互斥量用法。
std::mutex 是C++11 中最基本的互斥量,std::mutex 對象提供了獨占所有權的特性——即不支持遞歸地對 std::mutex 對象上鎖,而 std::recursive_lock 則可以遞歸地對互斥量對象上鎖。
std::mutex 的成員函數
- 構造函數,std::mutex不允許拷貝構造,也不允許 move 拷貝,最初產生的 mutex 對象是處於 unlocked 狀態的。
- lock(),調用線程將鎖住該互斥量。線程調用該函數會發生下面 3 種情況:(1). 如果該互斥量當前沒有被鎖住,則調用線程將該互斥量鎖住,直到調用 unlock之前,該線程一直擁有該鎖。(2). 如果當前互斥量被其他線程鎖住,則當前的調用線程被阻塞住。(3). 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
- unlock(), 解鎖,釋放對互斥量的所有權。
- try_lock(),嘗試鎖住互斥量,如果互斥量被其他線程占有,則當前線程也不會被阻塞。線程調用該函數也會出現下面 3 種情況,(1). 如果當前互斥量沒有被其他線程占有,則該線程鎖住互斥量,直到該線程調用 unlock 釋放互斥量。(2). 如果當前互斥量被其他線程鎖住,則當前調用線程返回 false,而並不會被阻塞掉。(3). 如果當前互斥量被當前調用線程鎖住,則會產生死鎖(deadlock)。
下面給出一個與 std::mutex 的小例子(參考)
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex volatile int counter(0); // non-atomic counter std::mutex mtx; // locks access to counter void attempt_10k_increases() { for (int i=0; i<10000; ++i) { if (mtx.try_lock()) { // only increase if currently not locked: ++counter; mtx.unlock(); } } } int main (int argc, const char* argv[]) { std::thread threads[10]; for (int i=0; i<10; ++i) threads[i] = std::thread(attempt_10k_increases); for (auto& th : threads) th.join(); std::cout << counter << " successful increases of the counter.\n"; return 0; }
std::recursive_mutex 介紹
std::recursive_mutex 與 std::mutex 一樣,也是一種可以被上鎖的對象,但是和 std::mutex 不同的是,std::recursive_mutex 允許同一個線程對互斥量多次上鎖(即遞歸上鎖),來獲得對互斥量對象的多層所有權,std::recursive_mutex 釋放互斥量時需要調用與該鎖層次深度相同次數的 unlock(),可理解為 lock() 次數和 unlock() 次數相同,除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。
std::time_mutex 介紹
std::time_mutex 比 std::mutex 多了兩個成員函數,try_lock_for(),try_lock_until()。
try_lock_for 函數接受一個時間范圍,表示在這一段時間范圍之內線程如果沒有獲得鎖則被阻塞住(與 std::mutex 的 try_lock() 不同,try_lock 如果被調用時沒有獲得鎖則直接返回 false),如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
try_lock_until 函數則接受一個時間點作為參數,在指定時間點未到來之前線程如果沒有獲得鎖則被阻塞住,如果在此期間其他線程釋放了鎖,則該線程可以獲得對互斥量的鎖,如果超時(即在指定時間內還是沒有獲得鎖),則返回 false。
下面的小例子說明了 std::time_mutex 的用法(參考)。
#include <iostream> // std::cout #include <chrono> // std::chrono::milliseconds #include <thread> // std::thread #include <mutex> // std::timed_mutex std::timed_mutex mtx; void fireworks() { // waiting to get a lock: each thread prints "-" every 200ms: while (!mtx.try_lock_for(std::chrono::milliseconds(200))) { std::cout << "-"; } // got a lock! - wait for 1s, then this thread prints "*" std::this_thread::sleep_for(std::chrono::milliseconds(1000)); std::cout << "*\n"; mtx.unlock(); } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(fireworks); for (auto& th : threads) th.join(); return 0; }
std::recursive_timed_mutex 介紹
和 std:recursive_mutex 與 std::mutex 的關系一樣,std::recursive_timed_mutex 的特性也可以從 std::timed_mutex 推導出來,感興趣的同鞋可以自行查閱。 ;-)
std::lock_guard 介紹
與 Mutex RAII 相關,方便線程對互斥量上鎖。例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::lock_guard #include <stdexcept> // std::logic_error std::mutex mtx; void print_even (int x) { if (x%2==0) std::cout << x << " is even\n"; else throw (std::logic_error("not even")); } void print_thread_id (int id) { try { // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception: std::lock_guard<std::mutex> lck (mtx); print_even(id); } catch (std::logic_error&) { std::cout << "[exception caught]\n"; } } int main () { std::thread threads[10]; // spawn 10 threads: for (int i=0; i<10; ++i) threads[i] = std::thread(print_thread_id,i+1); for (auto& th : threads) th.join(); return 0; }
std::unique_lock 介紹
與 Mutex RAII 相關,方便線程對互斥量上鎖,但提供了更好的上鎖和解鎖控制。例子(參考):
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock std::mutex mtx; // mutex for critical section void print_block (int n, char c) { // critical section (exclusive access to std::cout signaled by lifetime of lck): std::unique_lock<std::mutex> lck (mtx); for (int i=0; i<n; ++i) { std::cout << c; } std::cout << '\n'; } int main () { std::thread th1 (print_block,50,'*'); std::thread th2 (print_block,50,'$'); th1.join(); th2.join(); return 0; }
好了,本文暫時講到這里,還剩下 std::try_lock,std::lock,std::call_once 三個函數沒有講到,留在下一篇博客中講吧 ;-)
前面兩講《C++11 並發指南二(std::thread 詳解)》,《C++11 並發指南三(std::mutex 詳解)》分別介紹了 std::thread 和 std::mutex,相信讀者對 C++11 中的多線程編程有了一個最基本的認識,本文將介紹 C++11 標准中 <future> 頭文件里面的類和相關函數。
<future> 頭文件中包含了以下幾個類和函數:
- Providers 類:std::promise, std::package_task
- Futures 類:std::future, shared_future.
- Providers 函數:std::async()
- 其他類型:std::future_error, std::future_errc, std::future_status, std::launch.
std::promise 類介紹
promise 對象可以保存某一類型 T 的值,該值可被 future 對象讀取(可能在另外一個線程中),因此 promise 也提供了一種線程同步的手段。在 promise 對象構造時可以和一個共享狀態(通常是std::future)相關聯,並可以在相關聯的共享狀態(std::future)上保存一個類型為 T 的值。
可以通過 get_future 來獲取與該 promise 對象相關聯的 future 對象,調用該函數之后,兩個對象共享相同的共享狀態(shared state)
- promise 對象是異步 Provider,它可以在某一時刻設置共享狀態的值。
- future 對象可以異步返回共享狀態的值,或者在必要的情況下阻塞調用者並等待共享狀態標志變為 ready,然后才能獲取共享狀態的值。
下面以一個簡單的例子來說明上述關系
#include <iostream> // std::cout #include <functional> // std::ref #include <thread> // std::thread #include <future> // std::promise, std::future void print_int(std::future<int>& fut) { int x = fut.get(); // 獲取共享狀態的值. std::cout << "value: " << x << '\n'; // 打印 value: 10. } int main () { std::promise<int> prom; // 生成一個 std::promise<int> 對象. std::future<int> fut = prom.get_future(); // 和 future 關聯. std::thread t(print_int, std::ref(fut)); // 將 future 交給另外一個線程t. prom.set_value(10); // 設置共享狀態的值, 此處和線程t保持同步. t.join(); return 0; }
std::promise 構造函數
default (1) | promise();
|
---|---|
with allocator (2) | template <class Alloc> promise (allocator_arg_t aa, const Alloc& alloc);
|
copy [deleted] (3) | promise (const promise&) = delete;
|
move (4) | promise (promise&& x) noexcept;
|
- 默認構造函數,初始化一個空的共享狀態。
- 帶自定義內存分配器的構造函數,與默認構造函數類似,但是使用自定義分配器來分配共享狀態。
- 拷貝構造函數,被禁用。
- 移動構造函數。
另外,std::promise 的 operator= 沒有拷貝語義,即 std::promise 普通的賦值操作被禁用,operator= 只有 move 語義,所以 std::promise 對象是禁止拷貝的。
例子:
#include <iostream> // std::cout #include <thread> // std::thread #include <future> // std::promise, std::future std::promise<int> prom; void print_global_promise () { std::future<int> fut = prom.get_future(); int x = fut.get(); std::cout << "value: " << x << '\n'; } int main () { std::thread th1(print_global_promise); prom.set_value(10); th1.join(); prom = std::promise<int>(); // prom 被move賦值為一個新的 promise 對象. std::thread th2 (print_global_promise); prom.set_value (20); th2.join(); return 0; }
std::promise::get_future 介紹
該函數返回一個與 promise 共享狀態相關聯的 future 。返回的 future 對象可以訪問由 promise 對象設置在共享狀態上的值或者某個異常對象。只能從 promise 共享狀態獲取一個 future 對象。在調用該函數之后,promise 對象通常會在某個時間點准備好(設置一個值或者一個異常對象),如果不設置值或者異常,promise 對象在析構時會自動地設置一個 future_error 異常(broken_promise)來設置其自身的准備狀態。上面的例子中已經提到了 get_future,此處不再重復。
std::promise::set_value 介紹
generic template (1) | void set_value (const T& val);
void set_value (T&& val);
|
---|---|
specializations (2) | void promise<R&>::set_value (R& val); // when T is a reference type (R&)
void promise<void>::set_value (void); // when T is void |
設置共享狀態的值,此后 promise 的共享狀態標志變為 ready.
std::promise::set_exception 介紹
為 promise 設置異常,此后 promise 的共享狀態變標志變為 ready,例子如下,線程1從終端接收一個整數,線程2將該整數打印出來,如果線程1接收一個非整數,則為 promise 設置一個異常(failbit) ,線程2 在std::future::get 是拋出該異常。
#include <iostream> // std::cin, std::cout, std::ios #include <functional> // std::ref #include <thread> // std::thread #include <future> // std::promise, std::future #include <exception> // std::exception, std::current_exception void get_int(std::promise<int>& prom) { int x; std::cout << "Please, enter an integer value: "; std::cin.exceptions (std::ios::failbit); // throw on failbit try { std::cin >> x; // sets failbit if input is not int prom.set_value(x); } catch (std::exception&) { prom.set_exception(std::current_exception()); } } void print_int(std::future<int>& fut) { try { int x = fut.get(); std::cout << "value: " << x << '\n'; } catch (std::exception& e) { std::cout << "[exception caught: " << e.what() << "]\n"; } } int main () { std::promise<int> prom; std::future<int> fut = prom.get_future(); std::thread th1(get_int, std::ref(prom)); std::thread th2(print_int, std::ref(fut)); th1.join(); th2.join(); return 0; }
std::promise::set_value_at_thread_exit 介紹
設置共享狀態的值,但是不將共享狀態的標志設置為 ready,當線程退出時該 promise 對象會自動設置為 ready。如果某個 std::future 對象與該 promise 對象的共享狀態相關聯,並且該 future 正在調用 get,則調用 get 的線程會被阻塞,當線程退出時,調用 future::get 的線程解除阻塞,同時 get 返回 set_value_at_thread_exit 所設置的值。注意,該函數已經設置了 promise 共享狀態的值,如果在線程結束之前有其他設置或者修改共享狀態的值的操作,則會拋出 future_error( promise_already_satisfied )。
std::promise::swap 介紹
交換 promise 的共享狀態。
上一講《C++11 並發指南四(<future> 詳解一 std::promise 介紹)》主要介紹了 <future> 頭文件中的 std::promise 類,本文主要介紹 std::packaged_task。
std::packaged_task 包裝一個可調用的對象,並且允許異步獲取該可調用對象產生的結果,從包裝可調用對象意義上來講,std::packaged_task 與 std::function 類似,只不過 std::packaged_task 將其包裝的可調用對象的執行結果傳遞給一個 std::future 對象(該對象通常在另外一個線程中獲取 std::packaged_task 任務的執行結果)。
std::packaged_task 對象內部包含了兩個最基本元素,一、被包裝的任務(stored task),任務(task)是一個可調用的對象,如函數指針、成員函數指針或者函數對象,二、共享狀態(shared state),用於保存任務的返回值,可以通過 std::future 對象來達到異步訪問共享狀態的效果。
可以通過 std::packged_task::get_future 來獲取與共享狀態相關聯的 std::future 對象。在調用該函數之后,兩個對象共享相同的共享狀態,具體解釋如下:
- std::packaged_task 對象是異步 Provider,它在某一時刻通過調用被包裝的任務來設置共享狀態的值。
- std::future 對象是一個異步返回對象,通過它可以獲得共享狀態的值,當然在必要的時候需要等待共享狀態標志變為 ready.
std::packaged_task 的共享狀態的生命周期一直持續到最后一個與之相關聯的對象被釋放或者銷毀為止。下面一個小例子大致講了 std::packaged_task 的用法:
#include <iostream> // std::cout #include <future> // std::packaged_task, std::future #include <chrono> // std::chrono::seconds #include <thread> // std::thread, std::this_thread::sleep_for // count down taking a second for each value: int countdown (int from, int to) { for (int i=from; i!=to; --i) { std::cout << i << '\n'; std::this_thread::sleep_for(std::chrono::seconds(1)); } std::cout << "Finished!\n"; return from - to; } int main () { std::packaged_task<int(int,int)> task(countdown); // 設置 packaged_task std::future<int> ret = task.get_future(); // 獲得與 packaged_task 共享狀態相關聯的 future 對象. std::thread th(std::move(task), 10, 0); //創建一個新線程完成計數任務. int value = ret.get(); // 等待任務完成並獲取結果. std::cout << "The countdown lasted for " << value << " seconds.\n"; th.join(); return 0; }
執行結果為:
concurrency ) ./Packaged_Task1 10 9 8 7 6 5 4 3 2 1 Finished! The countdown lasted for 10 seconds.
std::packaged_task 構造函數
default (1) | packaged_task() noexcept; |
---|---|
initialization (2) | template <class Fn> explicit packaged_task (Fn&& fn); |
with allocator (3) | template <class Fn, class Alloc> explicit packaged_task (allocator_arg_t aa, const Alloc& alloc, Fn&& fn); |
copy [deleted] (4) | packaged_task (const packaged_task&) = delete; |
move (5) | packaged_task (packaged_task&& x) noexcept; |
std::packaged_task 構造函數共有 5 中形式,不過拷貝構造已經被禁用了。下面簡單地介紹一下上述幾種構造函數的語義:
- 默認構造函數,初始化一個空的共享狀態,並且該 packaged_task 對象無包裝任務。
- 初始化一個共享狀態,並且被包裝任務由參數 fn 指定。
- 帶自定義內存分配器的構造函數,與默認構造函數類似,但是使用自定義分配器來分配共享狀態。
- 拷貝構造函數,被禁用。
- 移動構造函數。
下面例子介紹了各類構造函數的用法:
#include <iostream> // std::cout #include <utility> // std::move #include <future> // std::packaged_task, std::future #include <thread> // std::thread int main () { std::packaged_task<int(int)> foo; // 默認構造函數. // 使用 lambda 表達式初始化一個 packaged_task 對象. std::packaged_task<int(int)> bar([](int x){return x*2;}); foo = std::move(bar); // move-賦值操作,也是 C++11 中的新特性. // 獲取與 packaged_task 共享狀態相關聯的 future 對象. std::future<int> ret = foo.get_future(); std::thread(std::move(foo), 10).detach(); // 產生線程,調用被包裝的任務. int value = ret.get(); // 等待任務完成並獲取結果. std::cout << "The double of 10 is " << value << ".\n"; return 0; }
與 std::promise 類似, std::packaged_task 也禁用了普通的賦值操作運算,只允許 move 賦值運算。
std::packaged_task::valid 介紹
檢查當前 packaged_task 是否和一個有效的共享狀態相關聯,對於由默認構造函數生成的 packaged_task 對象,該函數返回 false,除非中間進行了 move 賦值操作或者 swap 操作。
請看下例:
#include <iostream> // std::cout #include <utility> // std::move #include <future> // std::packaged_task, std::future #include <thread> // std::thread // 在新線程中啟動一個 int(int) packaged_task. std::future<int> launcher(std::packaged_task<int(int)>& tsk, int arg) { if (tsk.valid()) { std::future<int> ret = tsk.get_future(); std::thread (std::move(tsk),arg).detach(); return ret; } else return std::future<int>(); } int main () { std::packaged_task<int(int)> tsk([](int x){return x*2;}); std::future<int> fut = launcher(tsk,25); std::cout << "The double of 25 is " << fut.get() << ".\n"; return 0; }
std::packaged_task::get_future 介紹
返回一個與 packaged_task 對象共享狀態相關的 future 對象。返回的 future 對象可以獲得由另外一個線程在該 packaged_task 對象的共享狀態上設置的某個值或者異常。
請看例子(其實前面已經講了 get_future 的例子):
#include <iostream> // std::cout #include <utility> // std::move #include <future> // std::packaged_task, std::future #include <thread> // std::thread int main () { std::packaged_task<int(int)> tsk([](int x) { return x * 3; })); // package task std::future<int> fut = tsk.get_future(); // 獲取 future 對象. std::thread(std::move(tsk), 100).detach(); // 生成新線程並調用packaged_task. int value = fut.get(); // 等待任務完成, 並獲取結果. std::cout << "The triple of 100 is " << value << ".\n"; return 0; }
std::packaged_task::operator()(Args... args) 介紹
調用該 packaged_task 對象所包裝的對象(通常為函數指針,函數對象,lambda 表達式等),傳入的參數為 args. 調用該函數一般會發生兩種情況:
- 如果成功調用 packaged_task 所包裝的對象,則返回值(如果被包裝的對象有返回值的話)被保存在 packaged_task 的共享狀態中。
- 如果調用 packaged_task 所包裝的對象失敗,並且拋出了異常,則異常也會被保存在 packaged_task 的共享狀態中。
以上兩種情況都使共享狀態的標志變為 ready,因此其他等待該共享狀態的線程可以獲取共享狀態的值或者異常並繼續執行下去。
共享狀態的值可以通過在 future 對象(由 get_future獲得)上調用 get 來獲得。
由於被包裝的任務在 packaged_task 構造時指定,因此調用 operator() 的效果由 packaged_task 對象構造時所指定的可調用對象來決定:
- 如果被包裝的任務是函數指針或者函數對象,調用 std::packaged_task::operator() 只是將參數傳遞給被包裝的對象。
- 如果被包裝的任務是指向類的非靜態成員函數的指針,那么 std::packaged_task::operator() 的第一個參數應該指定為成員函數被調用的那個對象,剩余的參數作為該成員函數的參數。
- 如果被包裝的任務是指向類的非靜態成員變量,那么 std::packaged_task::operator() 只允許單個參數。
std::packaged_task::make_ready_at_thread_exit 介紹
該函數會調用被包裝的任務,並向任務傳遞參數,類似 std::packaged_task 的 operator() 成員函數。但是與 operator() 函數不同的是,make_ready_at_thread_exit 並不會立即設置共享狀態的標志為 ready,而是在線程退出時設置共享狀態的標志。
如果與該 packaged_task 共享狀態相關聯的 future 對象在 future::get 處等待,則當前的 future::get 調用會被阻塞,直到線程退出。而一旦線程退出,future::get 調用繼續執行,或者拋出異常。
注意,該函數已經設置了 promise 共享狀態的值,如果在線程結束之前有其他設置或者修改共享狀態的值的操作,則會拋出 future_error( promise_already_satisfied )。
std::packaged_task::reset() 介紹
重置 packaged_task 的共享狀態,但是保留之前的被包裝的任務。請看例子,該例子中,packaged_task 被重用了多次:
#include <iostream> // std::cout #include <utility> // std::move #include <future> // std::packaged_task, std::future #include <thread> // std::thread // a simple task: int triple (int x) { return x*3; } int main () { std::packaged_task<int(int)> tsk (triple); // package task std::future<int> fut = tsk.get_future(); std::thread (std::move(tsk), 100).detach(); std::cout << "The triple of 100 is " << fut.get() << ".\n"; // re-use same task object: tsk.reset(); fut = tsk.get_future(); std::thread(std::move(tsk), 200).detach(); std::cout << "Thre triple of 200 is " << fut.get() << ".\n"; return 0; }
std::packaged_task::swap() 介紹
交換 packaged_task 的共享狀態。
好了,std::packaged_task 介紹到這里,本文參考了 http://www.cplusplus.com/reference/future/packaged_task/ 相關的內容。后一篇文章我將向大家介紹 std::future,std::shared_future 以及 std::future_error,另外還會介紹 <future> 頭文件中的 std::async,std::future_category 函數以及相關枚舉類型。
上一講《C++11 並發指南四(<future> 詳解二 std::packaged_task 介紹)》主要介紹了 <future> 頭文件中的 std::packaged_task 類,本文主要介紹 std::future,std::shared_future 以及 std::future_error,另外還會介紹 <future> 頭文件中的 std::async,std::future_category 函數以及相關枚舉類型。
std::future 介紹
前面已經多次提到過 std::future,那么 std::future 究竟是什么呢?簡單地說,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::async() 函數。
- std::promise::get_future,get_future 為 promise 類的成員函數,詳見 C++11 並發指南四(<future> 詳解一 std::promise 介紹)。
- std::packaged_task::get_future,此時 get_future為 packaged_task 的成員函數,詳見C++11 並發指南四(<future> 詳解二 std::packaged_task 介紹)。
一個 std::future 對象只有在有效(valid)的情況下才有用(useful),由 std::future 默認構造函數創建的 future 對象不是有效的(除非當前非有效的 future 對象被 move 賦值另一個有效的 future 對象)。
在一個有效的 future 對象上調用 get 會阻塞當前的調用者,直到 Provider 設置了共享狀態的值或異常(此時共享狀態的標志變為 ready),std::future::get 將返回異步任務的值或異常(如果發生了異常)。
// future example #include <iostream> // std::cout #include <future> // std::async, std::future #include <chrono> // std::chrono::milliseconds // a non-optimized way of checking for prime numbers: bool is_prime(int x) { for (int i = 2; i < x; ++i) if (x % i == 0) return false; return true; } int main() { // call function asynchronously: std::future < bool > fut = std::async(is_prime, 444444443); // do something while waiting for function to set future: std::cout << "checking, please wait"; std::chrono::milliseconds span(100); while (fut.wait_for(span) == std::future_status::timeout) std::cout << '.'; bool x = fut.get(); // retrieve return value std::cout << "\n444444443 " << (x ? "is" : "is not") << " prime.\n"; return 0; }
std::future 成員函數
std::future 構造函數
std::future 一般由 std::async, std::promise::get_future, std::packaged_task::get_future 創建,不過也提供了構造函數,如下表所示:
default (1) | future() noexcept; |
---|---|
copy [deleted] (2) | future (const future&) = delete; |
move (3) | future (future&& x) noexcept; |
不過 std::future 的拷貝構造函數是被禁用的,只提供了默認的構造函數和 move 構造函數(注:C++ 新特新)。另外,std::future 的普通賦值操作也被禁用,只提供了 move 賦值操作。如下代碼所示:
std::future<int> fut; // 默認構造函數 fut = std::async(do_some_task); // move-賦值操作。
std::future::share()
返回一個 std::shared_future 對象(本文后續內容將介紹 std::shared_future ),調用該函數之后,該 std::future 對象本身已經不和任何共享狀態相關聯,因此該 std::future 的狀態不再是 valid 的了。
#include <iostream> // std::cout #include <future> // std::async, std::future, std::shared_future int do_get_value() { return 10; } int main () { std::future<int> fut = std::async(do_get_value); std::shared_future<int> shared_fut = fut.share(); // 共享的 future 對象可以被多次訪問. std::cout << "value: " << shared_fut.get() << '\n'; std::cout << "its double: " << shared_fut.get()*2 << '\n'; return 0; }
std::future::get()
std::future::get 一共有三種形式,如下表所示(參考):
generic template (1) | T get(); |
---|---|
reference specialization (2) | R& future<R&>::get(); // when T is a reference type (R&) |
void specialization (3) | void future<void>::get(); // when T is void |
當與該 std::future 對象相關聯的共享狀態標志變為 ready 后,調用該函數將返回保存在共享狀態中的值,如果共享狀態的標志不為 ready,則調用該函數會阻塞當前的調用者,而此后一旦共享狀態的標志變為 ready,get 返回 Provider 所設置的共享狀態的值或者異常(如果拋出了異常)。
請看下面的程序:
#include <iostream> // std::cin, std::cout, std::ios #include <functional> // std::ref #include <thread> // std::thread #include <future> // std::promise, std::future #include <exception> // std::exception, std::current_exception void get_int(std::promise<int>& prom) { int x; std::cout << "Please, enter an integer value: "; std::cin.exceptions (std::ios::failbit); // throw on failbit try { std::cin >> x; // sets failbit if input is not int prom.set_value(x); } catch (std::exception&) { prom.set_exception(std::current_exception()); } } void print_int(std::future<int>& fut) { try { int x = fut.get(); std::cout << "value: " << x << '\n'; } catch (std::exception& e) { std::cout << "[exception caught: " << e.what() << "]\n"; } } int main () { std::promise<int> prom; std::future<int> fut = prom.get_future(); std::thread th1(get_int, std::ref(prom)); std::thread th2(print_int, std::ref(fut)); th1.join(); th2.join(); return 0; }
std::future::valid()
檢查當前的 std::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。
#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; }
std::future::wait()
等待與當前std::future 對象相關聯的共享狀態的標志變為 ready.
如果共享狀態的標志不是 ready(此時 Provider 沒有在共享狀態上設置值(或者異常)),調用該函數會被阻塞當前線程,直到共享狀態的標志變為 ready。
一旦共享狀態的標志變為 ready,wait() 函數返回,當前線程被解除阻塞,但是 wait() 並不讀取共享狀態的值或者異常。下面的代碼說明了 std::future::wait() 的用法(參考)
#include <iostream> // std::cout #include <future> // std::async, std::future #include <chrono> // std::chrono::milliseconds // a non-optimized way of checking for prime numbers: bool do_check_prime(int x) // 為了體現效果, 該函數故意沒有優化. { for (int i = 2; i < x; ++i) if (x % i == 0) return false; return true; } int main() { // call function asynchronously: std::future < bool > fut = std::async(do_check_prime, 194232491); std::cout << "Checking...\n"; fut.wait(); std::cout << "\n194232491 "; if (fut.get()) // guaranteed to be ready (and not block) after wait returns std::cout << "is prime.\n"; else std::cout << "is not prime.\n"; return 0; }
執行結果如下:
concurrency ) ./Future-wait Checking... 194232491 is prime. concurrency )
std::future::wait_for()
與 std::future::wait() 的功能類似,即等待與該 std::future 對象相關聯的共享狀態的標志變為 ready,該函數原型如下:
template <class Rep, class Period> future_status wait_for (const chrono::duration<Rep,Period>& rel_time) const;
而與 std::future::wait() 不同的是,wait_for() 可以設置一個時間段 rel_time,如果共享狀態的標志在該時間段結束之前沒有被 Provider 設置為 ready,則調用 wait_for 的線程被阻塞,在等待了 rel_time 的時間長度后 wait_until() 返回,返回值如下:
返回值 | 描述 |
---|---|
future_status::ready | 共享狀態的標志已經變為 ready,即 Provider 在共享狀態上設置了值或者異常。 |
future_status::timeout | 超時,即在規定的時間內共享狀態的標志沒有變為 ready。 |
future_status::deferred | 共享狀態包含一個 deferred 函數。 |
請看下面的例子:
#include <iostream> // std::cout #include <future> // std::async, std::future #include <chrono> // std::chrono::milliseconds // a non-optimized way of checking for prime numbers: bool do_check_prime(int x) // 為了體現效果, 該函數故意沒有優化. { for (int i = 2; i < x; ++i) if (x % i == 0) return false; return true; } int main() { // call function asynchronously: std::future < bool > fut = std::async(do_check_prime, 194232491); std::cout << "Checking...\n"; std::chrono::milliseconds span(1000); // 設置超時間隔. // 如果超時,則輸出".",繼續等待 while (fut.wait_for(span) == std::future_status::timeout) std::cout << '.'; std::cout << "\n194232491 "; if (fut.get()) // guaranteed to be ready (and not block) after wait returns std::cout << "is prime.\n"; else std::cout << "is not prime.\n"; return 0; }
std::future::wait_until()
與 std::future::wait() 的功能類似,即等待與該 std::future 對象相關聯的共享狀態的標志變為 ready,該函數原型如下:
template <class Rep, class Period> future_status wait_until (const chrono::time_point<Clock,Duration>& abs_time) const;
而 與 std::future::wait() 不同的是,wait_until() 可以設置一個系統絕對時間點 abs_time,如果共享狀態的標志在該時間點到來之前沒有被 Provider 設置為 ready,則調用 wait_until 的線程被阻塞,在 abs_time 這一時刻到來之后 wait_for() 返回,返回值如下:
返回值 | 描述 |
---|---|
future_status::ready | 共享狀態的標志已經變為 ready,即 Provider 在共享狀態上設置了值或者異常。 |
future_status::timeout | 超時,即在規定的時間內共享狀態的標志沒有變為 ready。 |
future_status::deferred | 共享狀態包含一個 deferred 函數。 |
std::shared_future 介紹
std::shared_future 與 std::future 類似,但是 std::shared_future 可以拷貝、多個 std::shared_future 可以共享某個共享狀態的最終結果(即共享狀態的某個值或者異常)。shared_future 可以通過某個 std::future 對象隱式轉換(參見 std::shared_future 的構造函數),或者通過 std::future::share() 顯示轉換,無論哪種轉換,被轉換的那個 std::future 對象都會變為 not-valid.
std::shared_future 構造函數
std::shared_future 共有四種構造函數,如下表所示:
default (1) | shared_future() noexcept; |
---|---|
copy (2) | shared_future (const shared_future& x); |
move (3) | shared_future (shared_future&& x) noexcept; |
move from future (4) | shared_future (future<T>&& x) noexcept; |
最后 move from future(4) 即從一個有效的 std::future 對象構造一個 std::shared_future,構造之后 std::future 對象 x 變為無效(not-valid)。
std::shared_future 其他成員函數
std::shared_future 的成員函數和 std::future 大部分相同,如下(每個成員函數都給出了連接):
- operator=
-
賦值操作符,與 std::future 的賦值操作不同,std::shared_future 除了支持 move 賦值操作外,還支持普通的賦值操作。
- get
- 獲取與該 std::shared_future 對象相關聯的共享狀態的值(或者異常) 。
- valid
- 有效性檢查 。
- wait
- 等待與該 std::shared_future 對象相關聯的共享狀態的標志變為 ready 。
- wait_for
- 等待與該 std::shared_future 對象相關聯的共享狀態的標志變為 ready 。 (等待一段時間,超過該時間段wait_for 返回。)
- wait_until
- 等待與該 std::shared_future 對象相關聯的共享狀態的標志變為 ready。(在某一時刻前等待,超過該時刻 wait_until 返回。)
std::future_error 介紹
class future_error : public logic_error;
std::future_error 繼承子 C++ 標准異常體系中的 logic_error,有關 C++ 異常的繼承體系,請參考相關的C++教程 ;-)。
其他與 std::future 相關的函數介紹
與 std::future 相關的函數主要是 std::async(),原型如下:
unspecified policy (1) | template <class Fn, class... Args> future<typename result_of<Fn(Args...)>::type> async(Fn&& fn, Args&&... args); |
---|---|
specific policy (2) | template <class Fn, class... Args> future<typename result_of<Fn(Args...)>::type> async(launch policy, Fn&& fn, Args&&... args); |
上面兩組 std::async() 的不同之處是第一類 std::async 沒有指定異步任務(即執行某一函數)的啟動策略(launch policy),而第二類函數指定了啟動策略,詳見 std::launch 枚舉類型,指定啟動策略的函數的 policy 參數可以是launch::async,launch::deferred,以及兩者的按位或( | )。
std::async() 的 fn 和 args 參數用來指定異步任務及其參數。另外,std::async() 返回一個 std::future 對象,通過該對象可以獲取異步任務的值或異常(如果異步任務拋出了異常)。
下面介紹一下 std::async 的用法。
#include <stdio.h> #include <stdlib.h> #include <cmath> #include <chrono> #include <future> #include <iostream> double ThreadTask(int n) { std::cout << std::this_thread::get_id() << " start computing..." << std::endl; double ret = 0; for (int i = 0; i <= n; i++) { ret += std::sin(i); } std::cout << std::this_thread::get_id() << " finished computing..." << std::endl; return ret; } int main(int argc, const char *argv[]) { std::future<double> f(std::async(std::launch::async, ThreadTask, 100000000)); #if 0 while(f.wait_until(std::chrono::system_clock::now() + std::chrono::seconds(1)) != std::future_status::ready) { std::cout << "task is running...\n"; } #else while(f.wait_for(std::chrono::seconds(1)) != std::future_status::ready) { std::cout << "task is running...\n"; } #endif std::cout << f.get() << std::endl; return EXIT_SUCCESS; }
其他與 std::future 相關的枚舉類介紹
下面介紹與 std::future 相關的枚舉類型。與 std::future 相關的枚舉類型包括:
enum class future_errc;
enum class future_status;
enum class launch;
下面分別介紹以上三種枚舉類型:
std::future_errc 類型
std::future_errc 類型描述如下(參考):
類型 |
取值 |
描述 |
---|---|---|
broken_promise | 0 |
與該 std::future 共享狀態相關聯的 std::promise 對象在設置值或者異常之前一被銷毀。 |
future_already_retrieved | 1 |
與該 std::future 對象相關聯的共享狀態的值已經被當前 Provider 獲取了,即調用了 std::future::get 函數。 |
promise_already_satisfied | 2 |
std::promise 對象已經對共享狀態設置了某一值或者異常。 |
no_state | 3 |
無共享狀態。 |
std::future_status 類型(參考)
std::future_status 類型主要用在 std::future(或std::shared_future)中的 wait_for 和 wait_until 兩個函數中的。
類型 | 取值 |
描述 |
---|---|---|
future_status::ready | 0 |
wait_for(或wait_until) 因為共享狀態的標志變為 ready 而返回。 |
future_status::timeout | 1 |
超時,即 wait_for(或wait_until) 因為在指定的時間段(或時刻)內共享狀態的標志依然沒有變為 ready 而返回。 |
future_status::deferred | 2 |
共享狀態包含了 deferred 函數。 |
std::launch 類型
該枚舉類型主要是在調用 std::async 設置異步任務的啟動策略的。
類型 | 描述 |
---|---|
launch::async | Asynchronous: 異步任務會在另外一個線程中調用,並通過共享狀態返回異步任務的結果(一般是調用 std::future::get() 獲取異步任務的結果)。 |
launch::deferred | Deferred: 異步任務將會在共享狀態被訪問時調用,相當與按需調用(即延遲(deferred)調用)。 |
請看下例(參考):
#include <iostream> // std::cout #include <future> // std::async, std::future, std::launch #include <chrono> // std::chrono::milliseconds #include <thread> // std::this_thread::sleep_for void do_print_ten(char c, int ms) { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); std::cout << c; } } int main() { std::cout << "with launch::async:\n"; std::future < void >foo = std::async(std::launch::async, do_print_ten, '*', 100); std::future < void >bar = std::async(std::launch::async, do_print_ten, '@', 200); // async "get" (wait for foo and bar to be ready): foo.get(); bar.get(); std::cout << "\n\n"; std::cout << "with launch::deferred:\n"; foo = std::async(std::launch::deferred, do_print_ten, '*', 100); bar = std::async(std::launch::deferred, do_print_ten, '@', 200); // deferred "get" (perform the actual calls): foo.get(); bar.get(); std::cout << '\n'; return 0; }
在我的機器上執行結果:
with launch::async: *@**@**@**@**@*@@@@@ with launch::deferred: **********@@@@@@@@@@