C++ 的多執行序程式開發 Thread:基本使用

在很久以前,Heresy 有寫過一系列《簡易的程式平行化方法-OpenMP》的文章,算是一種很簡單的平行化程式的開發方法,對於要把迴圈平行化,算是一種最簡單的方法了~而後來 Heresy 也有介紹過 nVIDIA CUDA 或者 OpenCL 這類、以大量平行化為目標的 GPGPU 程式開發架構。

Data parallelism 和 Task parallelism

實際上,在程式開發時,所謂的「平行化」(parallelism),大致上可以分為「Data parallelism」(資料平行化、維基百科和「Task parallelism」(工作平行化、維基百科)兩大概念。

其中,「data parallelism」的使用最簡單的例子,就是把迴圈平行化、分成好幾個 thread(執行序)來同時進行計算;而在這樣執行時,每個 thread 的所做的事情其實都是相同的,只有計算的資料不同而已~像是 OpenMP 裡面的「parallel for」(參考)、OpenCL 的「Data-Parallel Execution Model」、或是 nVIDIA CUDA 這類的平行化方法,基本上都是屬於「data parallelism」的類型。

但是有的時候,可能會是想要讓幾個很複雜、而且不相同的計算同時進行,這時候就沒辦法使用「data parallelism」的概念、而是要使用「task parallelism」的方法了~他的基本概念,就是在程式裡面產生額外的 thread、來進行不同的、獨立的計算;像是 OpenMP 的「section」(參考),就是屬於「task parallelism」的方法。

C 11 Thread

雖然 OpenMP 已經有提供了 task parallelism 的功能,不過實際上它所提供的功能相當簡單、也不無法做進一步的控制,所以基本上應該也只能適用於較簡單的例子。而這邊 Heresy 要來介紹的,則是一個功能比較完整的 thread 控制的函示庫,那就是 C 11 的 STL 新加入的「Thread」(以下稱為「STL Thread」,官方文件MSDN)!

不過,雖然 STL Thread 是 C 11 標準函式庫的一部分,但是要注意的是,由於 C 11 還算是一個很新的標準,並非所有編譯器都有支援;像是 Visual C 2010 就還不支援、要等到下一代的 Visual Studio 2012 才有支援。所以如果是要在 MSVC10 這種還不支援 STL Thread 的開發環境下使用的話,可以考慮使用 Boost C Libraries 所提供的 Thread 函式庫(官方文件),他基本上是和 STL Thread 相同的(最大的差異只在於 namespace)(gcc 4.6 對 STL thread 的支援性似乎也還不是很好)

 

基本使用

如果只是要產生一個新的執行序來執行額外的程式的話,STL Thread 的基本使用其實相當簡單,大致上如下:

#include <iostream>
#include <thread>

using namespace std;

void test_func()
{
// do something
double dSum = 0;
for( int i = 0; i < 10000; i )
for( int j = 0; j < 10000; j )
dSum = i*j;

cout <<
"Thread: " << dSum << endl;
}

int main( int argc, char** argv )
{
// execute thread
thread mThread( test_func );

// do somthing
cout <<
"main thread" << endl;

// wait the thread stop
mThread.join();

return 0;
}

首先,STL Thread 的 header file 是 <thread>,在使用前必須要先 include 這個檔案。

而要產生新的 thread,基本上就是取去建立一個新的 std::thread 的物件,在這邊就是 mThread;而在建立 std::thread 的物件的時候,可以直接把一個可以呼叫的物件(callable object、一般是 function object)當作參數傳進去,這樣在 mThread 這個物件被建立出來的時候,系統就會產稱一個新的執行序、去執行所指定的 function object 了~而在這邊,就是 test_func() 這個函式。

而當新的執行序開始執行後,雖然電腦會開始執行 test_func() 裡面的計算,但是同時,他也會繼續執行下面的指令,在這邊就是透過 cout 輸出「main thread」這個字串。

可以看到,在這邊的範例裡,Heresy 是刻意把 test_func() 裡的計算寫得很複雜、用來拖時間。

main thread
Thread: 2.4995e 015

最後面去呼叫 mThreadjoin() 這個函式,則是用來告訴編譯器,在這邊要等 mThread 的計算工作完成後、才能繼續做下去;如此一來,可以避免 mThread 明明還在進行計算,但是主程式卻已經結束的問題。而如果之後的程式有要用到其他執行序的計算結果的話,也是要記得加上 join(),才能確定所需要的計算已經結束了。

而如果要執行的 function object 是需要參數的話,也可以直接在建立 std::thread 物件的時候,直接把參數附加在建構子裡。下面就是一個例子:

#include <iostream>
#include <thread>

using namespace std;

void test_func2( int i )
{
cout <<
i << endl;
}

int main( int argc, char** argv )
{
thread mThread( test_func2, 10 );
mThread.join();

return 0;
}

不過要注意的一點是,在把 callable object 傳遞給 STL Thread 開啟一個新的 thread 的時候,他會是採用複製的方法,把傳入的物件複製一份來用;所以如果在裡面有修改道本身的資料的話,就需要使用 std::ref() 來產生物件的參考、然後再傳進去。下面就是一個例子:

#include <iostream>
#include <thread>

using namespace std;

class funcObj
{
public:
int iData;

funcObj()
{
iData = 0;
}

void operator()()
{
iData;
}
};

int main( int argc, char** argv )
{
funcObj co;

// copy
thread mThread1( co );
mThread1.join();
cout << co.iData << endl;

// reference
thread mThread2( ref( co ) );
mThread2.join();
cout << co.iData << endl;

return 0;
}

這邊的 funcObj 就是一個有 call operator(operator()())的類別,他被呼叫的時候,會把內部的計數器(iData)的值加 1。而在主程式裡面,第一次使用 STL Thread 執行的時候,是直接把 funcObj 的物件(co)傳進去;這時候他會在內部複製一份來執行,所以當 mThread1 執行結束後,co 裡的 iData 的值並不會改變。

而當第二次執行的時候,由於傳進到 STL thread 建構子的物件是 ref( co ),所以實際上 mThread2 所執行的會是 co 這個 funcObj 物件的參考;也因此,co.iData 就會在 mThread2 裡被修改到,等到結束後,他的值就會變成 1 了~

 

其他功能

上面基本上就是 STL Thread 最基本的使用了~透過 std::thread 這個物件,基本上是可以相當簡單地開啟一個新的執行序來處理額外的計算,然後在目前的執行序、同時繼續做其他的計算的;而實務上,有需要的話,也是可以開許多個執行序來用的~

接下來這邊,則是一些也算是基礎的其他功能。

  • thread::hardware_concurrency()

    hardware_concurrency()std::thread 的 static member function,可以用來取得在硬體層面上可以同時執行的執行序的數量,基本上可以視為處理器的核心數目;不過實際上這只是估計值,如果無法判斷時,值會是 0。(參考

  • this_thread

    this_thread 是 STL thread 裡的一個特別的 namespace,底下提供 get_id()yield()sleep_until()sleep_for() 四個函式可以呼叫,都是針對目前的執行序進行操作的。

    其中,get_id() 可以用來取得目前的執行序的 id(型別是 thread::id);另一方面,也可以透過 std::thread 的物件的 get_id() 這個 member function 來取得(例如:mThread.get_id())。這個功能主要是可以用來識別不同的執行序,有的時候是用的到的。

    sleep_for()sleep_until() 則是用來讓目前的執行序暫時停下來的,前者是停止一段指定的時間、後者則是設定一個絕對時間、讓執行序在指定的時間再繼續執行;而時間的參數,則是要使用 std::chronoMSDN)的 duration範例)和 time_point範例)這兩種型別的時間資料。

    yield() 是暫時放棄一段 CPU 時間、讓給其他執行序使用的;這個應該算是比較進階的使用了,在這邊暫時跳過,之後有機會再整理。


這篇算是 STL Thread 最簡單的使用了。接下來,應該會再花點時間、整理一下中斷一個執行序、以及多執行序間同步的處理問題。不過,就要再等一段時間了。

2 thoughts on “C++ 的多執行序程式開發 Thread:基本使用”

發佈留言

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