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。這些…就等下一篇再來寫吧~


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

  1. 想請教一下怎麼寫,Thread才會是多執行緒?
    我是要寫矩陣相乘,但我發現不用Thread,還比使用Thread速度還快,所以我不知道是哪裡寫錯了。

    • 只要建立一個新的 thread、讓他去跑自己的工作,就是多執行緒。

      但是並不是所有程式都是和寫成多執行緒的。
      因為執行緒本身也會有一些額外的負擔,所以在不合適的狀況下去把程式改成多執行緒,只會讓效率變差。
      例如執行緒執行的工作所需時間過短,或是和其他執行緒有 lock 的狀況,都可能會導致效率的變差。

      以你說的矩陣相乘來說,如果矩陣不夠大,那寫成多執行緒可能就會是反效果。

  2. 您的文章真的是太有幫助了! 但您說使用 lock_guard「中間的過程可能有 exception 產生的話,還是有可能會產生 gMutex 永遠不會被解鎖的狀況」就我的理解及實驗, 應該不會有這種情況! 是否能請您舉例說明一下呢?

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料