在前一篇的基本使用裡,Heresy 已經大概提過怎麼使用 STL Thread 來建立一個新的執行序、執行指定的計算了;以基本的操作方法來說,要使用 STL Thread,就是:
- 透過建立一個新的 std::thread 物件、產生一個新的執行序
- 在必要時呼叫 std::thread 物件的 join() 函式,確保該執行序已結束
不過,比較簡單的寫法,就是讓多個執行序、各自去執行彼此之間不相關的工作,在這種情況下,其實通常不會有什麼大問題;但是,如果不同的執行序之間,有去修改到共用的變數的話,就可能會有因為不知道哪個執行續會先執行到、而有問題發生了~
像下面就是一個簡單的例子:
#include <iostream> #include <thread> using namespace std; void OutputValue( int n ) { cout << "Number:"; for( int i = 0; i < n; ++ i ) { this_thread::sleep_for( chrono::duration<int, std::milli>( 5 ) ); cout << " " << i; } cout << endl; } int main( int argc, char** argv ) { cout << "Normal function call" << endl; OutputValue( 3 ); OutputValue( 4 ); cout << "\nCall function with thread" << endl; thread mThread1( OutputValue, 3 ); thread mThread2( OutputValue, 4 ); mThread1.join(); mThread2.join(); cout << endl; return 0; }
在這個例子裡,主要是透過 OutputValue() 這個函式,透過 standard output stream、cout 來輸出 0 – n 的數值;不過這邊為了拉長函式執行的時間間隔,所以有刻意使用 this_thread::sleep_for() 來在每次輸出間、停頓 5 毫秒(ms)。而在主函式裡,一開始則是先用一般的函式呼叫方法來做呼叫兩次,接下來則是用 STL Thread 建立兩個執行續、個別執行 OutputValue()。而程式執行的結果,應該會是像這樣:
Normal function call Number: 0 1 2 Number: 0 1 2 3 Call function with thread Number:Number: 0 0 1 1 2 2 3
可以看到,一般呼叫兩次的話,會很正常地、輸出成兩行。但是如果是建立兩個執行序各自執行的話,則會因為都是透過 cout 來做輸出,所以結果都會混在一起、失去本來希望呈現的格式。
如果遇到這種共用資源,但是又想獨佔他的時候,該怎麼辦呢?在 STL Thread 裡,有提供一系列特別的類別、Mutual exclusion(縮寫為 mutex、維基百科),就是用來處理這種問題的。在 STL Thread 的 <mutex> 這個 header 檔裡,總共提供了四種 mutex 可以視不同的需求來使用,包括了 mutex、timed_mutex、recursive_mutex、recursive_timed_mutex;而如果要以基本的 mutex 來修改上面的程式的話,大致上就像下面這樣:
#include <iostream> #include <thread> #include <mutex> using namespace std; mutex gMutex; void OutputValue( int n ) { gMutex.lock(); cout << "Number:"; for( int i = 0; i < n; ++ i ) { this_thread::sleep_for( chrono::duration<int, std::milli>( 5 ) ); cout << " " << i; } cout << endl; gMutex.unlock(); } int main( int argc, char** argv ) { thread mThread1( OutputValue, 3 ); thread mThread2( OutputValue, 4 ); mThread1.join(); mThread2.join(); return 0; }
這邊的重點,就是透過一個全域的 mutex 變數 gMutex 來做控制,他主要就是透過 lock() 和 unlock() 這兩個函式,來設定變數的狀態是否被鎖定。而當在 OutputValue() 裡面呼叫了 gMutex 的 lock() 這個函式時,他就會去檢查 gMutex 是否已經被鎖定,如果沒有被鎖住的話,他就會把 gMutex 設定成鎖定、然後繼續執行;而如果已經被鎖住的話,他則會停在這邊、等到鎖定被解除、再把 gMutex 鎖住、繼續執行。
如此一來,修改過的 OutputValue() 就可以確保函式內的 cout 一次只會被一個執行序呼叫到了~當然啦,以這個例子來說,同時也就喪失了多執行序的意義就是了。 ^^"
不過實際上,上面直接使用 mutex 的 lock() 和 unlock(),並不是一個好辦法。因為如果 lock() 和 unlock() 之間,不小心因為 return 而離開 OutputValue(),就有可能出現有 lock()、但是沒有對應的 unlock() 的狀況!在這種狀況下,如果又有其他執行序在等著他被解鎖,那就會產生必須一直等下去、永遠不會結束的狀況了!
而要避免這樣的問題產生,最好是不要直接使用 mutex 的 lock() / unlock(),而是透過 lock_guard 這個 template class、來做 mutex 的控制;它的使用方法,就是:
void OutputValue( int n )
{
lock_guard<mutex> mLock( gMutex );
cout << "Number:";
for( int i = 0; i < n; ++ i )
{
this_thread::sleep_for( chrono::duration<int, std::milli>( 5 ) );
cout << " " << i;
}
cout << endl;
}
基本上,這邊就是透過一個型別是 lock_guad<mutex> 的物件 mLock、來管理全域變數 gMutex;當 mLock 被建立的同時,gMutex 就會被自動鎖定,而當 mLock 因為生命週期結束而消失時,gMutex 也會因此被自動解鎖~相較於前面手動使用 lock() 和 unlock(),使用 lock_guard 算是一個比較方便、也比較安全的方法。
不過要注意的是,如果中間的過程可能有 exception 產生的話,還是有可能會產生 gMutex 永遠不會被解鎖的狀況。
這邊大概就是 STL Thread 裡的 mutex 基本的使用方法了~
而實際上,除了這邊介紹的基本型的 mutex 外,STL 也還有提供三種有延伸功能的 mutex 類別可以使用,也就是前面有提到的 timed_mutex、recursive_mutex 和 recursive_timed_mutex ;而除了 lock_guard 外,STL 也還有另一個功能更多的類別,叫做 unique_lock。這些…就等下一篇再來寫吧~
[…] 關於 mutex 和 lock_guard 這部分的說明,則可以參考之前的《C++ 的多執行序程式開發 Thread:多執行序之間的溝通(一)》這篇文章。 […]
讚讚
感謝您的文章 受益良多 感謝!!
讚讚
有幫助就好 :)
讚讚
想請教一下怎麼寫,Thread才會是多執行緒?
我是要寫矩陣相乘,但我發現不用Thread,還比使用Thread速度還快,所以我不知道是哪裡寫錯了。
讚讚
只要建立一個新的 thread、讓他去跑自己的工作,就是多執行緒。
但是並不是所有程式都是和寫成多執行緒的。
因為執行緒本身也會有一些額外的負擔,所以在不合適的狀況下去把程式改成多執行緒,只會讓效率變差。
例如執行緒執行的工作所需時間過短,或是和其他執行緒有 lock 的狀況,都可能會導致效率的變差。
以你說的矩陣相乘來說,如果矩陣不夠大,那寫成多執行緒可能就會是反效果。
讚讚
感謝老師的分享
請問 thread的程式能夠回傳副程式的資料嗎?
讚讚
thread 執行的函式基本上是沒有回傳值的,需要他的處理結果的話,基本上都是讓她寫在某個變數裡面,之後再去讀取。
不然就是要使用其他架構來寫,例如 C++11 STD 的 future
http://en.cppreference.com/w/cpp/thread/future
讚讚
[…] 之前已經有用三篇文章,介紹了 C++ 11 裡、STL 的 Thread 這個函式庫的使用方法了。有興趣的話,請先參考《基本使用》、《多執行序之間的溝通(一)》和《多執行序之間的溝通(二)》這三篇文章。 […]
讚讚
您的文章真的是太有幫助了! 但您說使用 lock_guard「中間的過程可能有 exception 產生的話,還是有可能會產生 gMutex 永遠不會被解鎖的狀況」就我的理解及實驗, 應該不會有這種情況! 是否能請您舉例說明一下呢?
讚讚
恩…Heresy 也忘了那時候為啥這樣寫的了?
今天又找了一下資料,lock_guard 的確應該是可以對應 exception 的。
感謝你的回覆,把那行刪掉了。
參考: http://www.justsoftwaresolutions.co.uk/threading/multithreading-in-c++0x-part-4-protecting-shared-data.html
讚Liked by 1 person
[…] 發表迴響 在上一篇《多執行序之間的溝通(一)》裡,Heresy 已經大概介紹過,最簡單的 mutex 和 lock_guard […]
讚讚
[…] 多執行序之間的溝通(一) […]
讚讚