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:用于指定一個或多個變量是線程專用,后面會解釋線程專有和私有的區別。
?
相應的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函數以及說明:?
?
環境變量
?OpenMP提供了一些環境變量,用來在運行時對并行代碼的執行進行控制。這些環境變量可以控制:1)設置線程數;2)指定循環如何劃分;3)將線程綁定到處理器;4)啟用/禁用嵌套并行,設置最大的嵌套并行級別;5)啟用/禁用動態線程;6)設置線程堆棧大小;7)設置線程等待策略。常用的環境變量:
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,2、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
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
總結
以上是生活随笔為你收集整理的OpenMP用法大全的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: __new__ __init__区别
- 下一篇: python中的next()以及iter