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編程模型以線程爲基礎,經過編譯製導指令制導並行化,有三種編程要素能夠實現並行化控制,他們分別是編譯製導、API函數集和環境變量。併發

編譯器指令
OpenMP的編譯器指令的目標主要有:1)產生一個並行區域;2)劃分線程中的代碼塊;3)在線程之間分配循環迭代;4)序列化代碼段;5)同步線程間的工做。編譯製導指令以#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:用於指定一個或多個變量是線程專用,後面會解釋線程專有和私有的區別。ide

 
相應的OpenMP子句爲: 函數


private:指定一個或多個變量在每一個線程中都有它本身的私有副本;
firstprivate:指定一個或多個變量在每一個線程都有它本身的私有副本,而且私有變量要在進入並行域或任務分擔域時,繼承主線程中的同名變量的值做爲初值;
lastprivate:是用來指定將線程中的一個或多個私有變量的值在並行處理結束後複製到主線程中的同名變量中,負責拷貝的線程是for或sections任務分擔中的最後一個線程; 
reduction:用來指定一個或多個變量是私有的,而且在並行處理結束後這些變量要執行指定的歸約運算,並將結果返回給主線程同名變量;
nowait:指出併發線程能夠忽略其餘制導指令暗含的路障同步;
num_threads:指定並行域內的線程的數目; 
schedule:指定for任務分擔中的任務分配調度類型;
shared:指定一個或多個變量爲多個線程間的共享變量;
ordered:用來指定for任務分擔域內指定代碼段須要按照串行循環次序執行;
copyprivate:配合single指令,將指定線程的專有變量廣播到並行域內其餘線程的同名變量中;
copyin n:用來指定一個threadprivate類型的變量須要用主線程同名變量進行初始化;
default:用來指定並行域內的變量的使用方式,缺省是shared。性能

 

 
 

API函數
除上述編譯製導指令以外,OpenMP還提供了一組API函數用於控制併發線程的某些行爲,下面是一些經常使用的OpenMP API函數以及說明: ui

 

環境變量
 OpenMP提供了一些環境變量,用來在運行時對並行代碼的執行進行控制。這些環境變量能夠控制:1)設置線程數;2)指定循環如何劃分;3)將線程綁定處處理器;4)啓用/禁用嵌套並行,設置最大的嵌套並行級別;5)啓用/禁用動態線程;6)設置線程堆棧大小;7)設置線程等待策略。經常使用的環境變量:atom

OMP_SCHEDULE:用於for循環並行化後的調度,它的值就是循環調度的類型;  
OMP_NUM_THREADS:用於設置並行域中的線程數;   
OMP_DYNAMIC:經過設定變量值,來肯定是否容許動態設定並行域內的線程數;  
OMP_NESTED:指出是否能夠並行嵌套。 

OpenMP指令及子句用法
parallel 
parallel 是用來構造一個並行塊的,也可使用其餘指令如for、sections等和它配合使用。parallel指令是用來爲一段代碼建立多個線程來執行它的。parallel塊中的每行代碼都被多個線程重複執行。和傳統的建立線程函數比起來,至關於爲一個線程入口函數重複調用建立線程函數來建立線程並等待線程執行完。程序示例以下:

void fun1()

{

#pragma omp parallel num_threads(6)  //定義6個線程,每一個線程都將運行{}內代碼,運行結果:輸出6次Test

    {

        cout << "Test" << endl;

    }

    system("pause");

}

for
for指令則是用來將一個for循環分配到多個線程中執行。for指令通常能夠和parallel指令合起來造成parallel for指令使用,也能夠單獨用在parallel語句的並行塊中。parallel for用於生成一個並行域,並將計算任務在多個線程之間分配,用於分擔任務。程序示例以下:

void fun2()

{

#pragma omp parallel for num_threads(6)       {

        printf("OpenMP Test, 線程編號爲: %d\n", omp_get_thread_num());

    }                                     //指定了6個線程,迭代量爲12,每一個線程都分到了12/6=2次的迭代量。

    system("pause");

}

sections & section
section語句是用在sections語句裏用來將sections語句裏的代碼劃分紅幾個不一樣的段,每段都並行執行。語法格式以下:

#pragma omp [parallel] sections [子句]

{

   #pragma omp section

   {

            代碼塊

   } 

   #pragma omp section

   {

            代碼塊

   } 

}

說明各個section裏的代碼都是並行執行的,而且各個section被分配到不一樣的線程執行。

使用section語句時,須要注意的是這種方式須要保證各個section裏的代碼執行時間相差不大,不然某個section執行時間比其餘section過長就達不到並行執行的效果了。用for語句來分攤是由系統自動進行,只要每次循環間沒有時間上的差距,那麼分攤是很均勻的,使用section來劃分線程是一種手工劃分線程的方式。

private
private子句用於將一個或多個變量聲明成線程私有的變量,變量聲明成私有變量後,指定每一個線程都有它本身的變量私有副本,其餘線程沒法訪問私有副本。即便在並行區域外有同名的共享變量,共享變量在並行區域內不起任何做用,而且並行區域內不會操做到外面的共享變量。程序示例以下:

 int k = 100;

#pragma omp parallel for private(k)

         for ( k=0; k < 3; k++)

         {

                   printf("k=%d/n", k);

         }

         printf("last k=%d/n", k);

上面程序執行後打印的結果以下:

k=0

k=1

k=2

k=3

last k=100

從打印結果能夠看出,for循環前的變量k和循環區域內的變量k實際上是兩個不一樣的變量。用private子句聲明的私有變量的初始值在並行區域的入口處是未定義的,它並不會繼承同名共享變量的值。

private聲明的私有變量不能繼承同名變量的值,但實際狀況中有時須要繼承原有共享變量的值,OpenMP提供了firstprivate子句來實現這個功能。若上述程序使用firstprivate(k),則並行區域內的私有變量k繼承了外面共享變量k的值100做爲初始值,而且在退出並行區域後,共享變量k的值保持爲100未變。

有時在並行區域內的私有變量的值通過計算後,在退出並行區域時,須要將它的值賦給同名的共享變量,前面的private和firstprivate子句在退出並行區域時都沒有將私有變量的最後取值賦給對應的共享變量,lastprivate子句就是用來實如今退出並行區域時將私有變量的值賦給共享變量。程序示例以下:

 int k = 100;

#pragma omp parallel for firstprivate(k),lastprivate(k)

         for ( i=0; i < 4; i++)

         {

                   k+=i;

                   printf("k=%d/n",k);

         }

         printf("last k=%d/n", k);

上面代碼執行後的打印結果以下:

k=100

k=101

k=103

k=102

last k=103

從打印結果能夠看出,退出for循環的並行區域後,共享變量k的值變成了103,而不是保持原來的100不變。OpenMP規範中指出,若是是循環迭代,那麼是將最後一次循環迭代中的值賦給對應的共享變量;若是是section構造,那麼是最後一個section語句中的值賦給對應的共享變量。注意這裏說的最後一個section是指程序語法上的最後一個,而不是實際運行時的最後一個運行完的。若是是類(class)類型的變量使用在lastprivate參數中,那麼使用時有些限制,須要一個可訪問的,明確的缺省構造函數,除非變量也被使用做爲firstprivate子句的參數;還須要一個拷貝賦值操做符,而且這個拷貝賦值操做符對於不一樣對象的操做順序是未指定的,依賴於編譯器的定義。

threadprivate
threadprivate指令用來指定全局的對象被各個線程各自複製了一個私有的拷貝,即各個線程具備各自私有的全局對象。threadprivate和private的區別在於threadprivate聲明的變量一般是全局範圍內有效的,而private聲明的變量只在它所屬的並行構造中有效。用做threadprivate的變量的地址不能是常數。對於C++的類(class)類型變量,用做threadprivate的參數時有些限制,當定義時帶有外部初始化時,必須具備明確的拷貝構造函數。程序示例以下:

int g;

#pragma omp threadprivate(g)       //必定要先聲明

int main(int argc, char *argv[])

{

       /* Explicitly turn off dynamic threads */

       omp_set_dynamic(0);

#pragma omp parallel

       {

              g = omp_get_thread_num();   

              printf("tid: %d\n",g);         //隨機依次輸出0~3

       } // End of parallel region

 

#pragma omp parallel

       {

              int temp = g*g;

              printf("tid : %d, tid*tid: %d\n",g, temp);  //不一樣線程中全局變量值不一樣

       } // End of parallel region

}

注意:在使用threadprivate的時候,要用omp_set_dynamic(0)關閉動態線程的屬性,才能保證結果正確。

Share
shared子句能夠用於聲明一個或多個變量爲共享變量。所謂的共享變量,是值在一個並行區域的team內的全部線程只擁有變量的一個內存地址,全部線程訪問同一地址。因此,對於並行區域內的共享變量,須要考慮數據競爭條件,要防止競爭,須要增長對應的保護。程序示例以下:

#define COUNT     10000

int main(int argc, _TCHAR* argv[])

{

       int sum = 0;

#pragma omp parallel for shared(sum)

       for(int i = 0; i < COUNT;i++)

       {

              sum = sum + i;

       }

       printf("%d\n",sum);

       return 0;

}

屢次運行,結果可能不同。須要注意的是:循環迭代變量在循環構造區域裏是私有的,聲明在循環構造區域內的自動變量都是私有的。若是循環迭代變量也是共有的,OpenMP該如何去執行,因此也只能是私有的了。即便使用shared來修飾循環迭代變量,也不會改變循環迭代變量在循環構造區域中是私有的這一特色。程序示例以下:
#define COUNT     10

int main(int argc, _TCHAR* argv[])

{

       int sum = 0;

       int i = 0;

#pragma omp parallel for shared(sum, i)

       for(i = 0; i < COUNT;i++)

       {

              sum = sum + i;

       }

       printf("%d\n",i);

       printf("%d\n",sum);

       return 0;

}

上述程序中,循環迭代變量i的輸出值爲0,儘管這裏使用shared修飾變量i。注意,這裏的規則只是針對循環並行區域,對於其餘的並行區域沒有這樣的要求。同時在循環並行區域內,循環迭代變量是不可修改的。即在上述程序中,不能再for循環體內對循環迭代變量i進行修改。

Default
default指定並行區域內變量的屬性,C++的OpenMP中default的參數只能爲shared或none。default(shared):表示並行區域內的共享變量在不指定的狀況下都是shared屬性

default(none):表示必須顯式指定全部共享變量的數據屬性,不然會報錯,除非變量有明確的屬性定義(好比循環並行區域的循環迭代變量只能是私有的)若是一個並行區域,沒有使用default子句,那麼其默認行爲爲default(shared)。

Copyin
copyin子句用於將主線程中threadprivate變量的值拷貝到執行並行區域的各個線程的threadprivate變量中,從而使得team內的子線程都擁有和主線程一樣的初始值。程序示例以下:

#include <omp.h> 

int A = 100; 

#pragma omp threadprivate(A) 

int main(int argc, _TCHAR* argv[]) 

#pragma omp parallel for 

    for(int i = 0; i<10;i++) 

    { 

        A++; 

        printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);   // #1 

    } 

    printf("Global A: %d\n",A); // 並行區域外的打印的「Globa A」的值老是和前面的thread 0的結果相等,由於退出並行區域後,只有master線程即0號線程運行。

 

#pragma omp parallel for copyin(A)

    for(int i = 0; i<10;i++) 

    { 

        A++; 

        printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);   // #1 

    } 

 

    printf("Global A: %d\n",A); // #2 

 

    return 0; 

}

不使用copyin的狀況下,進入第二個並行區域的時候,不一樣線程的私有副本A的初始值是不同的,這裏使用了copyin以後,發現全部的線程的初始值都使用主線程的值初始化,而後繼續運算,輸出的值即爲本次thread 0的結果。簡單理解,在使用了copyin後,全部的線程的threadprivate類型的副本變量都會與主線程的副本變量進行一次「同步」。 另外copyin中的參數必須被聲明成threadprivate的,對於類類型的變量,必須帶有明確的拷貝賦值操做符。

Copyprivate
copyprivate子句用於將線程私有副本變量的值從一個線程廣播到執行同一併行區域的其餘線程的同一變量。copyprivate只能用於single指令(single指令:用在一段只被單個線程執行的代碼段以前,表示後面的代碼段將被單線程執行)的子句中,在一個single塊的結尾處完成廣播操做。copyprivate只能用於private/firstprivate或threadprivate修飾的變量。程序示例以下:

int counter = 0;

#pragma omp threadprivate(counter)

int increment_counter()

{

         counter++;

         return(counter);

}

#pragma omp parallel

         {

                   int    count;

#pragma omp single copyprivate(counter)

                   {

                            counter = 50;

                   }

                   count = increment_counter();

                   printf("ThreadId: %ld, count = %ld/n", omp_get_thread_num(), count);

}

打印結果爲:

ThreadId: 2, count = 51

ThreadId: 0, count = 51

ThreadId: 3, count = 51

ThreadId: 1, count = 51

若是沒有使用copyprivate子句,那麼打印結果爲:

ThreadId: 2, count = 1

ThreadId: 1, count = 1

ThreadId: 0, count = 51

ThreadId: 3, count = 1

能夠看出,使用copyprivate子句後,single構造內給counter賦的值被廣播到了其餘線程裏,但沒有使用copyprivate子句時,只有一個線程得到了single構造內的賦值,其餘線程沒有獲取single構造內的賦值。

OpenMP中的任務調度
OpenMP中,任務調度主要用於並行的for循環中,當循環中每次迭代的計算量不相等時,若是簡單地給各個線程分配相同次數的迭代的話,會形成各個線程計算負載不均衡,這會使得有些線程先執行完,有些後執行完,形成某些CPU核空閒,影響程序性能。OpenMP提供了schedule子句來實現任務的調度。schedule子句格式:schedule(type,[size])。

  參數type是指調度的類型,能夠取值爲static,dynamic,guided,runtime四種值。其中runtime容許在運行時肯定調度類型,所以實際調度策略只有前面三種。

  參數size表示每次調度的迭代數量,必須是整數。該參數是可選的。當type的值是runtime時,不可以使用該參數。

靜態調度static
大部分編譯器在沒有使用schedule子句的時候,默認是static調度。static在編譯的時候就已經肯定了,那些循環由哪些線程執行。假設有n次循環迭代,t個線程,那麼給每一個線程靜態分配大約n/t次迭代計算。n/t不必定是整數,所以實際分配的迭代次數可能存在差1的狀況。

在不使用size參數時,分配給每一個線程的是n/t次連續的迭代,若循環次數爲10,線程數爲2,則線程0獲得了0~4次連續迭代,線程1獲得5~9次連續迭代。

當使用size時,將每次給線程分配size次迭代。若循環次數爲10,線程數爲2,指定size爲2則0、1次迭代分配給線程0,二、3次迭代分配給線程1,以此類推。

動態調度dynamic
  動態調度依賴於運行時的狀態動態肯定線程所執行的迭代,也就是線程執行完已經分配的任務後,會去領取還有的任務(與靜態調度最大的不一樣,每一個線程完成的任務數量可能不同)。因爲線程啓動和執行完的時間不肯定,因此迭代被分配到哪一個線程是沒法事先知道的。

  當不使用size 時,是將迭代逐個地分配到各個線程。當使用size 時,逐個分配size個迭代給各個線程,這個用法相似靜態調度。

啓發式調度guided
   採用啓發式調度方法進行調度,每次分配給線程迭代次數不一樣,開始比較大,之後逐漸減少。開始時每一個線程會分配到較大的迭代塊,以後分配到的迭代塊會逐漸遞減。迭代塊的大小會按指數級降低到指定的size大小,若是沒有指定size參數,那麼迭代塊大小最小會降到1。

  size表示每次分配的迭代次數的最小值,因爲每次分配的迭代次數會逐漸減小,少到size時,將再也不減小。具體採用哪種啓發式算法,須要參考具體的編譯器和相關手冊的信息。

調度方式總結
靜態調度static:每次哪些循環由那個線程執行時固定的,編譯調試。因爲每一個線程的任務是固定的,可是可能有的循環任務執行快,有的慢,不能達到最優。

動態調度dynamic:根據線程的執行快慢,已經完成任務的線程會自動請求新的任務或者任務塊,每次領取的任務塊是固定的。

啓發式調度guided:每一個任務分配的任務是先大後小,指數降低。當有大量任務須要循環時,剛開始爲線程分配大量任務,最後任務很少時,給每一個線程少許任務,能夠達到線程任務均衡。

OpenMP程序設計技巧總結
1.當循環次數較少時,若是分紅過多的線程來執行的話,可能會使得總的運行時間高於較少線程或一個線程的執行狀況,而且會增長能耗;

2.若是設置的線程數量遠大於CPU的核數的話,那麼存在着大量的任務切換和調度的開銷,也會下降總體的效率。

3.在嵌套循環中,若是外層循環迭代次數較少時,若是未來CPU核數增長到必定程度時,建立的線程數將可能小於CPU核數。另外若是內層循環存在負載平衡的狀況下,很難調度外層循環使之達到負載平衡。--------------------- 做者:ArrowYL 來源:CSDN 原文:https://blog.csdn.net/ArrowYL/article/details/81094837 版權聲明:本文爲博主原創文章,轉載請附上博文連接!

相關文章
相關標籤/搜索