在很久以前,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&amp;gt;
#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
最後面去呼叫 mThread 的 join() 這個函式,則是用來告訴編譯器,在這邊要等 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::chrono(MSDN)的 duration(範例)和 time_point(範例)這兩種型別的時間資料。
yield() 是暫時放棄一段 CPU 時間、讓給其他執行序使用的;這個應該算是比較進階的使用了,在這邊暫時跳過,之後有機會再整理。
這篇算是 STL Thread 最簡單的使用了。接下來,應該會再花點時間、整理一下中斷一個執行序、以及多執行序間同步的處理問題。不過,就要再等一段時間了。
😀
谢谢
非常趕寫前輩指導