C++ 的多執行序程式開發 Thread:多執行序之間的溝通(一)

在前一篇的基本使用裡,Heresy 已經大概提過怎麼使用 STL Thread 來建立一個新的執行序、執行指定的計算了;以基本的操作方法來說,要使用 STL Thread,就是:

  1. 透過建立一個新的 std::thread 物件、產生一個新的執行序
  2. 在必要時呼叫 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 可以視不同的需求來使用,包括了 mutextimed_mutexrecursive_mutexrecursive_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() 裡面呼叫了 gMutexlock() 這個函式時,他就會去檢查 gMutex 是否已經被鎖定,如果沒有被鎖住的話,他就會把 gMutex 設定成鎖定、然後繼續執行;而如果已經被鎖住的話,他則會停在這邊、等到鎖定被解除、再把 gMutex 鎖住、繼續執行。

如此一來,修改過的 OutputValue() 就可以確保函式內的 cout 一次只會被一個執行序呼叫到了~當然啦,以這個例子來說,同時也就喪失了多執行序的意義就是了。 ^^"

不過實際上,上面直接使用 mutexlock()unlock(),並不是一個好辦法。因為如果 lock()unlock() 之間,不小心因為 return 而離開 OutputValue(),就有可能出現有 lock()、但是沒有對應的 unlock() 的狀況!在這種狀況下,如果又有其他執行序在等著他被解鎖,那就會產生必須一直等下去、永遠不會結束的狀況了!

而要避免這樣的問題產生,最好是不要直接使用 mutexlock() / 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_mutexrecursive_mutexrecursive_timed_mutex ;而除了 lock_guard 外,STL 也還有另一個功能更多的類別,叫做 unique_lock。這些…就等下一篇再來寫吧~

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。