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》也可以。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM