C++11多線程教學(二)
C++11多線程教學II
從我最近發布的C++11線程教學文章里,我們已經知道C++11線程寫法與POSIX的pthreads寫法相比,更為簡潔。只需很少幾個簡單概念,我們就能搭建相當復雜的處理圖片程序,但是我們回避了線程同步的議題。在接下來的部分,我們將進入C++11多線程編程的同步領域,看看如何來同步一組并行的線程。
我們快速回顧一下如何利用c++11創建線程組。上次教學當中,我們用傳統c數組保存線程,也完全可以用標準庫的向量容器,這樣做更有c++11的氣象,同時又能避免使用new和delete來動態分配內存所帶來的隱患。
#include
#include
#include
//This function will be called from a thread線程將調用此函數
void func(int tid) {
std::cout << "Launched by thread " << tid << std::endl;
}
int main() {
std::vectorth;
int nr_threads = 10;
//Launch a group of threads 啟動一組線程
for (int i = 0; i < nr_threads; ++i) {
th.push_back(std::thread(func,i));
}
//Join the threads with the main thread 與主線程協同運轉
for(auto &t : th){
t.join();
}
return 0;
}
在Mac OSX Lion上用clang++或gcc-4.7編譯上述程序:
clang++ -Wall -std=c++0x -stdlib=libc++ file_name.cpp
g++-4.7 -Wall -std=c++11 file_name.cpp
現代Linux系統上,使用gcc-4.6.x編譯代碼:
g++ -std=c++0x -pthread file_name.cpp
某些活生生的現實問題,其棘手的地方就在于它們天然就是并行方式,上面開頭部分寫的代碼當中,已用很簡化的語法實現了這種方式。舉一個典型的并行問題:引入兩個數組,一個數組與乘數相乘,生成孟得伯特集合。
線程之間還有同步層次的問題。以向量點乘為例來說,兩個等長(維度)向量,他們的元素兩兩對應相乘,然后乘積相加得到一個標量結果。初略的并行編碼方式如下:
#include
#include
#include
...
void dot_product(const std::vector&v1, const std::vector&v2, int &result,
int L, int R){
for(int i = L; i < R; ++i){
result += v1[i] * v2[i];
}
}
int main(){
int nr_elements = 100000;
int nr_threads = 2;
int result = 0;
std::vectorthreads;
//Fill two vectors with some constant values for a quick verification
// v1={1,1,1,1,...,1}以常量值填充兩個向量,便于檢驗
// v2={2,2,2,2,...,2}
// The result of the dot_product should be 200000 for this particular case
//當前例子的點乘結果應為200000
std::vectorv1(nr_elements,1), v2(nr_elements,2);
//Split nr_elements into nr_threads parts 把nr_elements份計算任務劃分為 nr_threads 個部分
std::vectorlimits = bounds(nr_threads, nr_elements);
//Launch nr_threads threads: 啟動 nr_threads 條線程
for (int i = 0; i < nr_threads; ++i) {
threads.push_back(std::thread(dot_product, std::ref(v1), std::ref(v2),
std::ref(result), limits[i], limits[i+1]));
}
//Join the threads with the main thread 協同 線程組與主線程
for(auto &t : threads){
t.join();
}
//Print the result打印結果
std::cout<<result<<std::endl;
return 0;
}
上述代碼的結果顯然應該是200000,但是運行幾次出來的結果都有輕微的差異:
sol $g++-4.7 -Wall -std=c++11 cpp11_threads_01.cpp
sol $./a.out
138832
sol $./a.out
138598
sol $./a.out
138032
sol $./a.out
140690
sol $
怎么回事?仔細看第九行代碼,變量result累加v1[i],v2[i]之和。該行是典型的競爭條件,這段代碼在兩個異步線程中并行運作,變量result可以被任意一方搶先訪問而被改變。
通過規定該變量應同步地由線程來訪問,我們可以避免出問題,我們可以采用一個mutex(互斥)來達成目的,mutex是一種特別用途的變量,行為如同一個barrier,同步化訪問那段修改result變量的代碼:
#include
#include
#include
#include
static std::mutex barrier;
...
void dot_product(const std::vector&v1, const std::vector&v2, int &result, int L, int R){
int partial_sum = 0;
for(int i = L; i < R; ++i){
partial_sum += v1[i] * v2[i];
}
std::lock_guardblock_threads_until_finish_this_job(barrier);
result += partial_sum;
}
...
第6行創建一個全局mutex變量barrier,第15行強制線程在完成for循環之后才同步存取result。注意,這一次我們采用了新的變量partial sum,聲明為線程局部變量。其他代碼部分保持原貌。
針對這個特定的例子,我們還可以找到更簡潔優美的方案,我們可以采用原子類型,這是一種特定的變量類型,能達成安全的同時讀寫,在底層基本上解決了同步問題。額外注明一下,我們可以使用的原子類型只能用在原子操作上,這些操作都定義在atomic 頭文件里面:
#include
#include
#include
#include
void dot_product(const std::vector&v1, const std::vector&v2, std::atomic&result, int L, int R){
int partial_sum = 0;
for(int i = L; i < R; ++i){
partial_sum += v1[i] * v2[i];
}
result += partial_sum;
}
int main(){
int nr_elements = 100000;
int nr_threads = 2;
std::atomicresult(0);
std::vectorthreads;
...
return 0;
}
蘋果機的clang++當前還不支持原子類型和原子操作,有兩個辦法可以達到目標,編譯最新clang++源碼,要么使用最新的gcc-4.7,也需要編譯源碼。
想學習c++11新語法,我推薦閱讀《Professional C++》第二版,《C++ Primer Plus》也可以。