天天看點

并行程式設計OpenMP基礎及簡單示例

OpenMP基本概念

OpenMP是一種用于共享記憶體并行系統的多線程程式設計方案,支援的程式設計語言包括C、C++和Fortran。OpenMP提供了對并行算法的高層抽象描述,特别适合在多核CPU機器上的并行程式設計。編譯器根據程式中添加的pragma指令,自動将程式并行處理,使用OpenMP降低了并行程式設計的難度和複雜度。當編譯器不支援OpenMP時,程式會退化成普通(串行)程式。程式中已有的OpenMP指令不會影響程式的正常編譯運作。

在VS中啟用OpenMP很簡單,很多主流的編譯環境都内置了OpenMP。在項目上右鍵->屬性->配置屬性->C/C++->語言->OpenMP支援,選擇“是”即可。

OpenMP執行模式

OpenMP采用fork-join的執行模式。開始的時候隻存在一個主線程,當需要進行并行計算的時候,派生出若幹個分支線程來執行并行任務。當并行代碼執行完成之後,分支線程會合,并把控制流程交給單獨的主線程。

一個典型的fork-join執行模型的示意圖如下:

并行程式設計OpenMP基礎及簡單示例

OpenMP程式設計模型以線程為基礎,通過編譯制導指令制導并行化,有三種程式設計要素可以實作并行化控制,他們分别是編譯制導、API函數集和環境變量。

編譯制導

編譯制導指令以#pragma omp 開始,後邊跟具體的功能指令,格式如:#pragma omp 指令[子句[,子句] …]。常用的功能指令如下:

parallel:用在一個結構塊之前,表示這段代碼将被多個線程并行執行;

for:用于for循環語句之前,表示将循環計算任務配置設定到多個線程中并行執行,以實作任務分擔,必須由程式設計人員自己保證每次循環之間無資料相關性;

parallel for:parallel和for指令的結合,也是用在for循環語句之前,表示for循環體的代碼将被多個線程并行執行,它同時具有并行域的産生和任務分擔兩個功能;

sections:用在可被并行執行的代碼段之前,用于實作多個結構塊語句的任務分擔,可并行執行的代碼段各自用section指令标出(注意區分sections和section);

parallel sections:parallel和sections兩個語句的結合,類似于parallel for;

single:用在并行域内,表示一段隻被單個線程執行的代碼;

critical:用在一段代碼臨界區之前,保證每次隻有一個OpenMP線程進入;

flush:保證各個OpenMP線程的資料影像的一緻性;

barrier:用于并行域内代碼的線程同步,線程執行到barrier時要停下等待,直到所有線程都執行到barrier時才繼續往下執行;

atomic:用于指定一個資料操作需要原子性地完成;

master:用于指定一段代碼由主線程執行;

threadprivate:用于指定一個或多個變量是線程專用,後面會解釋線程專有和私有的差別。

相應的OpenMP子句為: 

private:指定一個或多個變量在每個線程中都有它自己的私有副本;

firstprivate:指定一個或多個變量在每個線程都有它自己的私有副本,并且私有變量要在進入并行域或任務分擔域時,繼承主線程中的同名變量的值作為初值;

lastprivate:是用來指定将線程中的一個或多個私有變量的值在并行處理結束後複制到主線程中的同名變量中,負責拷貝的線程是for或sections任務分擔中的最後一個線程; 

reduction:用來指定一個或多個變量是私有的,并且在并行處理結束後這些變量要執行指定的歸約運算,并将結果傳回給主線程同名變量;

nowait:指出并發線程可以忽略其他制導指令暗含的路障同步;

num_threads:指定并行域内的線程的數目; 

schedule:指定for任務分擔中的任務配置設定排程類型;

shared:指定一個或多個變量為多個線程間的共享變量;

ordered:用來指定for任務分擔域内指定代碼段需要按照串行循環次序執行;

copyprivate:配合single指令,将指定線程的專有變量廣播到并行域内其他線程的同名變量中;

copyin:用來指定一個threadprivate類型的變量需要用主線程同名變量進行初始化;

default:用來指定并行域内的變量的使用方式,預設是shared。

API函數

除上述編譯制導指令之外,OpenMP還提供了一組API函數用于控制并發線程的某些行為,下面是一些常用的OpenMP API函數以及說明: 

并行程式設計OpenMP基礎及簡單示例

環境變量

OpenMP中定義一些環境變量,可以通過這些環境變量控制OpenMP程式的行為,常用的環境變量:

OMP_SCHEDULE:用于for循環并行化後的排程,它的值就是循環排程的類型;  

OMP_NUM_THREADS:用于設定并行域中的線程數;  

OMP_DYNAMIC:通過設定變量值,來确定是否允許動态設定并行域内的線程數;  

OMP_NESTED:指出是否可以并行嵌套。 

簡單示例之parallel使用

parallel制導指令用來建立并行域,後邊要跟一個大括号将要并行執行的代碼放在一起:

執行以上程式有如下輸出:

并行程式設計OpenMP基礎及簡單示例

程式列印出了4個“Test”,說明parallel後的語句被4個線程分别執行了一次,4個是程式預設的線程數,還可以通過子句num_threads顯式控制建立的線程數:

編譯運作有如下輸出:

并行程式設計OpenMP基礎及簡單示例

程式中顯式定義了6個線程,是以parallel後的語句塊分别被執行了6次。第二行的空行是由于每個線程都是獨立運作的,在其中一個線程輸出字元“Test”之後還沒有來得及換行時,另一個線程直接輸出了字元“Test”。

簡單示例之parallel for使用

使用parallel制導指令隻是産生了并行域,讓多個線程分别執行相同的任務,并沒有實際的使用價值。parallel for用于生成一個并行域,并将計算任務在多個線程之間配置設定,進而加快計算運作的速度。可以讓系統預設配置設定線程個數,也可以使用num_threads子句指定線程個數。

運作輸出:

并行程式設計OpenMP基礎及簡單示例

上邊程式指定了6個線程,疊代量為12,從輸出可以看到每個線程都分到了12/6=2次的疊代量。

OpenMP效率提升以及不同線程數效率對比

以上程式分别指定了2、4、8、12個線程和不使用OpenMP優化來執行一段垃圾程式,輸出如下:

并行程式設計OpenMP基礎及簡單示例

可見,使用OpenMP優化後的程式執行時間是原來的1/4左右,并且并不是線程數使用越多效率越高,一般線程數達到4~8個的時候,不能簡單通過提高線程數來進一步提高效率。