《OpenMP编译原理及实现技术》摘录
內容摘自《OpenMP編譯原理及實現技術》第2章
代碼測試環境:Windows7 64bit, VS2010, 4核機。
可以說OpenMP制導指令將C語言擴展為一個并行語言,但OpenMP本身不是一種獨立的并行語言,而是為多處理器上編寫并行程序而設計的、指導共享內存、多線程并行的編譯制導指令和應用程序編程接口(API),可在C/C++和Fortran中應用,并在串行代碼中以編譯器可識別的注釋形式出現。OpenMP標準是由一些具有國際影響力的軟件和硬件廠商共同定義和提出。
1.?OpenMP基本概念
1.1 OpenMP執行模式
OpenMP的執行模型采用fork-join的形式,其中fork創建線程或者喚醒已有線程;join即多線程的會合。fork-join執行模型在剛開始執行的時候,只有一個稱為“主線程”的運行線程存在。主線程在運行過程中,當遇到需要進行并行計算的時候,派生出線程來執行并行任務。在并行執行的時候,主線程和派生線程共同工作。在并行代碼執行結束后,派生線程退出或者阻塞,不再工作,控制流程回到單獨的主線程中。OpenMP線程:在OpenMP程序中用于完成計算任務的一個執行流的執行實體,可以是操作系統的線程也可以是操作系統上的進程。
1.2 OpenMP編程要素
OpenMP編程模型以線程為基礎,通過編譯制導指令來顯示地指導并行化,OpenMP為編程人員提供了三種編程要素來實現對并行化的完善控制。它們是編譯制導、API函數集和環境變量。
1.2.1?編譯制導:在C/C++程序中,OpenMP的所有編譯制導指令是以#pragma omp開始,后面跟具體的功能指令(或命令)。其中指令或命令是可以單獨出現的,而子句則必須出現在制導指令之后。制導指令和子句按照功能可以大體上分成四類:(1)、并行域控制類;(2)、任務分擔類;(3)、同步控制類;(4)、數據環境類。并行域控制類指令用于指示編譯器產生多個線程以并發執行任務,任務分擔類指令指示編譯器如何給各個并發線程分發任務,同步控制類指令指示編譯器協調并發線程之間的時間約束關系,數據環境類指令處理并行域內外的變量共享或私有屬性以及邊界上的數據傳送操作等。
編譯制導指令:版本為2.5的OpenMP規范中的指令:(1)、parallel:用在一個結構塊之前,表示這段代碼將被多個線程并行執行;(2)、for:用于for循環語句之前,表示將循環計算任務分配到多個線程中并行執行,以實現任務分擔,必須由編程人員自己保證每次循環之間無數據相關性;(3)、parallel for:parallel和for指令的結合,也是用在for循環語句之前,表示for循環體的代碼將被多個線程并行執行,它同時具有并行域的產生和任務分擔兩個功能;(4)、sections:用在可被并行執行的代碼段之前,用于實現多個結構塊語句的任務分擔,可并行執行的代碼段各自用section指令標出;(5)、parallel sections:parallel和sections兩個語句的結合,類似于parallel for;(6)、single:用在并行域內,表示一段只被單個線程執行的代碼;(7)、critical:用在一段代碼臨界區之前,保證每次只有一個OpenMP線程進入;(8)、flush:保證各個OpenMP線程的數據影像的一致性;(9)、barrier:用于并行域內代碼的線程同步,線程執行到barrier時要停下等待,直到所有線程都執行到barrier時才能繼續往下執行;(10)、atomic:用于指定一個數據操作需要原子性地完成;(11)、master:用于指定一段代碼由主線程執行;(12)、threadprivate:用于指定一個或多個變量是線程專用。
OpenMP的子句:(1)、private:指定一個或多個變量在每個線程中都有它自己的私有副本;(2)、firstprivate:指定一個或多個變量在每個線程都有它自己的私有副本,并且私有變量要在進入并行域或認為分擔域時,繼承主線程中的同名變量的值作為初值;(3)、lastprivate:是用來指定將線程中的一個或多個私有變量的值在并行處理結束后復制到主線程中的同名變量中,負責拷貝的線程是for或sections任務分擔中的最后一個線程;(4)、reduction:用來指定一個或多個變量是私有的,并且在并行處理結束后這些變量要執行指定的歸約運算,并將結果返回給主線程同名變量;(5)、nowait:指出并發線程可以忽略其他制導指令暗含的路障同步;(6)、num_threads:指定并行域內的線程的數目;(7)、schedule:指定for任務分擔中的任務分配調度類型;(8)、shared:指定一個或多個變量為多個線程間的共享變量;(9)、copyprivate:配合single指令,將指定線程的專有變量廣播到并行域內其他線程的同名變量中;(10)、copyin:用來指定一個threadprivate類型的變量需要用主線程同名變量進行初始化;(11)、default:用來指定并行域內的變量的使用方式,缺省是shared。
1.2.2 API函數集:用于控制并發線程的某些行為。
(1)、omp_in_parallel:判斷當前是否在并行域中;(2)、omp_get_thread_num:返回線程號;(3)、omp_set_num_threads:設置后續并行域中的線程個數;(4)、omp_get_num_threads:返回當前并行區域中的線程數;(5)、omp_get_max_threads:獲取并行域可用的最大線程數目;(6)、omp_get_num_procs:返回系統中處理器個數;(7)、omp_get_dynamic:判斷是否支持動態改變線程數目;(8)、omp_set_dynamic:啟用或關閉線程數目的動態改變;(9)、omp_get_nested:判斷系統是否支持并行嵌套;(10)、omp_set_nested:啟用或關閉并行嵌套;(11)、omp_init(_nest)_lock:初始化一個(嵌套)鎖;(12)、omp_destroy(_nest)_lock:銷毀一個(嵌套)鎖;(13)、omp_set(_nest)_lock:(嵌套)加鎖操作;(14)、omp_unset(_nest)_lock:(嵌套)解鎖操作;(15)、omp_test(_nest)_lock:非阻塞的(嵌套)加鎖;(16)、omp_get_wtime:獲取wall time時間;(17)、omp_set_wtime:設置wall time時間。
1.2.3 環境變量:可以在一定程度上控制OpenMP程序的行為。
(1)、OMP_SCHEDULE:用于for循環并行化后的調度,它的值就是循環調度的類型;(2)、OMP_NUM_THREADS:用于設置并行域中的線程數;(3)、OMP_DYNAMIC:通過設定變量值,來確定是否允許動態設定并行域內的線程數;(4)、OMP_NESTED:指出是否可以并行嵌套。
1.2.4 ICV:OpenMP規范中定義了一些內部控制變量ICV(Internal Control Variable),用于表示系統的屬性、能力和狀態等,可以通過OpenMP API函數訪問也可以通過環境變量進行修改。但是變量的具體名字和實現方式可以由各個編譯器自行決定。
2. OpenMP編程
2.1 并行域管理:在OpenMP的相鄰的fork、join操作之間稱之為一個并行域,并行域可以嵌套。
2.1.1 parallel的使用方法:
#pragma ompparallel[for | sections][子句[子句]…]
{ … 代碼 …}
parallel語句后面要用一個大括號對將要并行執行的代碼括起來。為了指定使用多少個線程來執行,可以通過設置環境變量OMP_NUM_THREADS或者調用omp_set_num_threads()函數,也可以使用num_threads子句,前者只能在程序剛開始運行時起作用,而API函數和子句可以在程序中并行域產生之前起作用。
#pragma omp parallel //parallel語句后面要用一個大括號對將要并行執行的代碼括起來
{//并行域的開始(對應fork)printf("hello, world\n");
}//并行域的結束(對應join)
#pragma omp parallel num_threads(8)
{printf("hello, world! Threadid=%d\n", omp_get_thread_num());
}
hello, world! Threadid=0
hello, world! Threadid=4
hello, world! Threadid=2
hello, world! Threadid=3
hello, world! Threadid=6
hello, world! Threadid=7
hello, world! Threadid=1
hello, world! Threadid=5
從Threadid的不同可以看出創建了8個線程來執行以上代碼。所以parallel指令是用來產生或喚醒多個線程創建并行域的,并且可以用num_threads子句控制線程數目。parallel域中的每行代碼都被多個線程重復執行。和傳統的創建線程函數比起來,其過程非常簡單直觀。parallel的并行域內部代碼中,若再出現parallel制導指令則出現并行域嵌套問題,如果設置了OMP_NESTED環境變量,那么在條件許可時內部并行域也會由多個線程執行,反之沒有設置相應變量,那么內部并行域的代碼將只有一個線程來執行。還有一個環境變量OMP_DYNAMIC也影響并行域的行為,如果沒有設置該環境變量將不允許動態調整并行域內的線程數目,omp_set_dynamic()也是用于同樣的目的。
2.2 任務分擔:當使用parellel制導指令產生出并行域之后,如果僅僅是多個線程執行完全相同的任務,那么只是徒增計算工作量而不能達到加速計算的目的,甚至可能相互干擾得到錯誤結果。因此在產生并行域之后,緊接著的問題就是如何將計算任務在這些線程之間分配,并加快計算結果的產生速度及其保證正確性。OpenMP可以完成的任務分擔的指令只有for、sections和single,嚴格意義上來說只有for和sections是任務分擔指令,而single只是協助任務分擔的指令。任務分擔域和并行域的定義一樣,既是指代碼區間也是指執行時間區間。
2.2.1 for制導指令:for指令指定緊隨它的循環語句必須由線程組并行執行,用來將一個for循環任務分配到多個線程,此時各個線程各自分擔其中一部分工作。for指令一般可以和parallel指令合起來形成parallel for指令使用,也可以單獨用在parallel指令的并行域中。
int j=0;#pragma omp for
for (j=0; j<4; ++j) {printf("j=%d, Threadid=%d\n", j, omp_get_thread_num());
}
j=0, Threadid=0
j=1, Threadid=0
j=2, Threadid=0
j=3, Threadid=0
從結果可以看出,4次循環都在一個Threadid為0的線程里執行,并沒有實現并發執行也不會加快計算速度。可見for指令要和parallel指令結合起來使用才有效果,即for出現在并行域中才能有多個線程來分擔任務。
int j=0;#pragma omp parallel for
for (j=0; j<4; ++j) {printf("j=%d, Threadid=%d\n", j, omp_get_thread_num());
}
j=0, Threadid=0
j=1, Threadid=1
j=3, Threadid=3
j=2, Threadid=2
int j=0;
#pragma omp parallel
{#pragma omp forfor (j=0; j<100; ++j) {//do someting}#pragma omp forfor (j=0; j<100; ++j) {//do someting}//do someting
}
此時只有一個并行域,在該并行域內的多個線程首先完成第一個for語句的任務分擔,然后在此進行一次同步(for制導指令本身隱含有結束處的路障同步),然后再進行第二個for語句的任務分擔,直到退出并行域只剩下一個主線程為止。
2.2.2 for調度:當循環中每次迭代的計算量不相等時,如果簡單地給各個線程分配相同次數的迭代的話,會使得各個線程計算負載不均衡,這會使得有些線程先執行完,有些后執行完,造成某些CPU核空閑,影響程序性能。在OpenMP的for任務分擔中,任務的劃分稱為調度,各個線程如何劃分任務是可以調整的,因此有靜態劃分、動態劃分等,所以調度也分成多個類型。for任務調度子句只能用于for制導指令中。在OpenMP中,對for循環任務調度使用schedule子句來實現。schedule子句使用格式為:schedule(type[, size]). type參數,表示調度類型,有四種調度類型如下:static、dynamic、guided、runtime。size參數為可選,表示以循環迭代次數計算的劃分單位,每個線程所承擔的計算任務對應于0個或若干個size次循環,size參數必須是整數。static、dynamic、guided三種調度方式都可以使用size參數,也可以不使用size參數。當type參數類型為runtime時,size參數是非法的。
2.2.2.1 static靜態調度:當for或者parallelfor編譯制導指令沒有帶schedule子句時,大部分系統中默認采用size為1的static調度方式。
int i=0;#pragma omp parallel for schedule(static)
for (i=0; i<10; ++i) {printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
i=0, thread_id=0
i=1, thread_id=0
i=3, thread_id=1
i=4, thread_id=1
i=5, thread_id=1
i=2, thread_id=0
i=6, thread_id=2
i=7, thread_id=2
i=8, thread_id=3
i=9, thread_id=3
注意:由于多線程執行時序的隨機性,每次執行時打印的結果順序可能存在差別。
int i=0;
#pragma omp parallel for schedule(static, 2)
for (i=0; i<10; ++i) {printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
i=0, thread_id=0
i=1, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=6, thread_id=3
i=7, thread_id=3
i=2, thread_id=1
i=3, thread_id=1
i=4, thread_id=2
i=5, thread_id=2
使用size參數時,分配給每個線程的size次連續的迭代計算。
2.2.2.2 dynamic動態調整:是動態地將迭代分配到各個線程,各線程動態的申請任務,因此較快的線程可能申請更多次數,而較慢的線程申請任務次數可能較少,因此動態調整可以在一定程度上避免前面提到的按循環次數劃分引起的負載不平衡問題。
int i=0;#pragma omp parallel for schedule(dynamic)
for (i=0; i<10; ++i) {printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
i=0, thread_id=0
i=4, thread_id=0
i=5, thread_id=0
i=6, thread_id=0
i=7, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=1, thread_id=2
i=2, thread_id=1
i=3, thread_id=3
int i=0;#pragma omp parallel for schedule(dynamic, 2)
for (i=0; i<10; ++i) {printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=0
i=5, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=6, thread_id=3
i=7, thread_id=3
動態調整時,size小有利于實現更好的負載均衡,但是會引起過多的任務動態申請的開銷,反之size大則開銷較少,但是不易于實現負載平衡,size的選擇需要在這兩者之間進行權衡。
2.2.2.3 guided調度:是一種采用指導性的啟發式自調度方法。開始時每個線程會分配到較大的迭代塊,之后分配到的迭代塊會逐漸遞減。迭代塊的大小會按指數級下降到指定的size大小,如果沒有指定size參數,那么迭代塊大小最小會降到1.
int i=0;#pragma omp parallel for schedule(guided, 2)
for (i=0; i<10; ++i) {printf("i=%d, thread_id=%d\n", i, omp_get_thread_num());
}
i=0, thread_id=1
i=1, thread_id=1
i=2, thread_id=1
i=9, thread_id=1
i=3, thread_id=0
i=4, thread_id=0
i=7, thread_id=3
i=8, thread_id=3
i=5, thread_id=2
i=6, thread_id=2
2.2.2.4 runtime調度:它不像static、dynamic、guided三種調度方式那樣是真實調度方式。它是在運行時根據環境變量OMP_SCHEDULE來確定調度類型,最終使用的調度類型仍然是static、dynamic、guided中的一種。
2.2.3 sections編譯制導指令:是用于非迭代計算的任務分擔,它將sections語句里的代碼用section制導指令劃分成幾個不同的段(可以是一條語句,也可以是用{…}括起來的結構塊),不同的section段由不同的線程并行執行。
#pragma omp parallel sections
{#pragma omp sectionprintf("section 1 thread=%d\n", omp_get_thread_num());#pragma omp sectionprintf("section 2 thread=%d\n", omp_get_thread_num());#pragma omp sectionprintf("section 3 thread=%d\n", omp_get_thread_num());
}
section 1 thread=0
section 2 thread=2
section 2 thread=1
#pragma omp parallel
{#pragma omp sections{#pragma omp sectionprintf("section 1 Threadid=%d\n", omp_get_thread_num());#pragma omp sectionprintf("section 2 Threadid=%d\n", omp_get_thread_num());}#pragma omp sections//兩個sections構造先后串行執行,與for制導指令一樣,在sections的結束處有一個隱含的路障同步{ #pragma omp sectionprintf("section 3 Threadid=%d\n", omp_get_thread_num());#pragma omp sectionprintf("section 4 Threadid=%d\n", omp_get_thread_num());}
}
section 1 thread=0
section 2 thread=1
section 3 thread=0
section 4 thread=1
這里有兩個sections構造先后串行執行的,即第二個sections構造的代碼要等第一個sections構造的代碼執行完后才能執行。sections構造里面的各個section部分代碼是并行執行的。與for制導指令一樣,在sections的結束處有一個隱含的路障同步,沒有其他說明的情況下,所有線程都必須到達該點才能往下運行。使用section指令時,需要注意的是這種方式需要保證各個section里的代碼執行時間相差不大,否則某個section執行時間比其他section過長就造成了其它線程空閑等待的情況。用for語句來分擔任務時工作量由系統自動劃分,只要每次循環間沒有時間上的差異,那么分攤是比較均勻的,使用section來劃分線程是一種手工劃分工作量的方式,最終負載均衡的好壞得依賴于程序員。
2.2.4 single制導指令:單線程執行single制導指令指定所包含的代碼只由一個線程執行,別的線程跳過這段代碼。如果沒有nowait從句,所有線程在single制導指令結束處隱式同步點同步。如果single制導指令有nowait從句,則別的線程直接向下執行,不在隱式同步點等待;single制導指令用在一段只被單個線程執行的代碼段之前,表示后面的代碼段將被單線程執行。#pragma omp single[子句]
#pragma omp parallel
{#pragma omp singleprintf("Beginning work1.\n");printf("work on 1 parallelly.%d\n", omp_get_thread_num());#pragma omp singleprintf("Finishing work1.\n");#pragma omp single nowaitprintf("Beginning work2.\n");printf("work on 2 parallelly.%d\n", omp_get_thread_num());
}
Beginning work1.
work on 1 parallelly.2
Finishing work1.
work on 1 parallelly.1
work on 1 parallelly.0
work on 1 parallelly.3
Beginning work2.
work on 2 parallelly.1
work on 2 parallelly.0
work on 2 parallelly.2
work on 2 parallelly.3
另一種需要使用single制導指令的情況是為了減少并行域創建和撤銷的開銷,而將多個臨界的parallel并行域合并時。經過合并后,原來并行域之間的串行代碼也將被并行執行,違反了代碼原來的目的,因此這部分代碼可以用single指令加以約束只用一個線程來完成。
2.3 同步:在正確產生并行域并用for、sections等語句進行任務分擔后,還須考慮的是這些并發線程的同步互斥需求。在OpenMP應用程序中,由于是多線程執行,所以必須有線程互斥機制以保證程序在出現數據競爭的時候能夠得出正確的結果,并且能夠控制線程執行的先后制約關系,以保證執行結果的正確性。OpenMP支持兩種不同類型的線程同步機制,一種是互斥鎖的機制,可以用來保護一塊共享的存儲空間,使任何時候訪問這塊共享內存空間的線程最多只有一個,從而保證了數據的完整性;另外一種同步機制是事件同步機制,這種機制保證了多個線程之間的執行順序。互斥的操作針對需要保護的數據而言,在產生了數據競爭的內存區域加入互斥,可以使用包括critical、atomic等制導指令以及API中的互斥函數。而事件機制則控制線程執行順序,包括barrier同步路障、ordered定序區段、master主線程執行等。
2.3.1 critical臨界區:在可能產生內存數據訪問競爭的地方,都需要插入相應的臨界區制導指令,格式:#pragam omp critical[(name)] ?critical語句不允許互相嵌套。
int i;
int max_num_x=max_num_y=-1;#pragma omp parallel for
for (i=0; i<n; ++i) {#pragma omp critical(max_arx);if (arx[i] >max_num_x) {max_num_x = arx[i];}#pragma omp critical(max_ary)if (ary[i]>max_num_y) {max_num_y = ary[i];}
}
在一個并行域內的for任務分擔域中,各個線程逐個進入到critical保護的區域內,比較當前元素和最大值的關系并可能進行最大值的更替,從而避免了數據競爭的情況。
2.3.2 atomic原子操作:critical臨界區操作能夠作用在任意大小的代碼塊上,而原子操作只能作用在單條賦值語句中。能夠使用原子語句的前提條件是相應的語句能夠轉化成一條機器指令,使得相應的功能能夠一次執行完畢而不會被打斷。C/C++中可用的原子操作:“+、-、*、/、&、^、<<、>>”。值得注意的是,當對一個數據進行原子操作保護的時候,就不能對數據進行臨界區的保護,OpenMP運行時并不能在這兩種保護機制之間建立配合機制。用戶在針對同一個內存單元使用原子操作的時候,需要在程序所有涉及到該變量并行賦值的部位都加入原子操作的保護。
int counter = 0;#pragma omp parallel
{for (int i=0; i<10000; ++i) {#pragma omp atomic//atomic operationcounter++;}
}printf("counter=%d\n", counter);
counter=4000
由于使用atomic語句,則避免了可能出現的數據訪問競爭情況,最后的執行結果都是一致的。而將atomic這一行語句從源程序中刪除時,由于有了數據訪問的競爭情況,所以最后的執行結果是不確定的。
2.3.3 barrier同步路障:路障(barrier)是OpenMP線程的一種同步方法。線程遇到路障時必須等待,直到并行區域內的所有線程都到達了同一點,才能繼續執行下面的代碼。在每一個并行域和任務分擔域的結束處都會有一個隱含的同步路障,執行此并行域/任務分擔域的線程組在執行完畢本區域代碼之前,都需要同步并行域的所有線程。也就是說在parallel、for、sections和single構造的最后,會有一個隱式的路障。在有些情況下,隱含的同步路障并不能提供有效的同步措施。這時,需要程序員插入明確的同步路障語句#pragma omp barrier。此時,在并行區域的執行過程中,所有的執行線程都會在同步路障語句上進行同步。
#pragma omp parallel
{Initialization();#pragma omp barrierProcess();
}
只有等所有的線程都完成Initialization()初始化操作以后,才能夠進行下一步的處理動作,因此,在此處插入一個明確的同步路障操作以實現線程之間的同步。
2.3.4 nowait:為了避免在循環過程中不必要的同步路障并加快運行速度,可以使用nowait子句除去這個隱式的路障。
int i, j;#pragma omp parallel num_threads(4)
{#pragma omp for nowaitfor (int i=0; i<8; ++i) {printf("+\n");}#pragma omp forfor (j=0; j<8; ++j) {printf("-\n");}
}
+
+
+
+
-
+
+
-
-
-
-
-
+
+
-
-
此時,線程在完成第一個for循環子任務后,并不需要同步等待,而是直接執行后面的任務,因此出現“-”在“+”前面的情況。nowait子句消除了不必要的同步開銷,加快了計算速度,但是也引入了實現上的困難。
2.3.5 master主線程執行:用于指定一段代碼由主線程執行。master制導指令和single制導指令類似,區別在于,master制導指令包含的代碼段只由主線程執行,而single制導指令包含的代碼段可由任一線程執行,并且master制導指令在結束處沒有隱式同步,也不能指定nowait從句。
int a[5], i;#pragma omp parallel
{#pragma omp forfor (i=0; i<5; ++i) {a[i] = i * i;}#pragma omp masterfor (i=0; i<5; ++i) {printf("a[%d]=%d\n", i, a[i]);}
}
a[0]=0
a[1]=1
a[2]=4
a[3]=9
a[4]=16
只有一個線程將逐個元素打印出來。
2.3.6 ordered順序制導指令:對于循環代碼的任務分擔中,某些代碼的執行需要按規定的順序執行。典型的情況如下:在一次循環的過程中大部分的工作是可以并行執行的,而特定部分代碼的工作需要等到前面的工作全部完成之后才能夠執行。這時,可以使用ordered子句使特定的代碼按照串行循環的次序來執行。
#pragma omp parallel
{#pragma omp forfor (i=0; i<100; ++i) {//一些無數據相關、可并行亂序執行的操作//do someting#pragma omp ordered//一些有數據相關、只能順序執行的操作//do someting}
}
雖然在ordered子句之前的工作是并行執行的,但是在遇到ordered子句的時候,只有前面的循環都執行完畢之后,才能夠進行下一步執行。這樣一來,有些任務在并行執行,對于部分必須串行執行的部分才啟用ordered保護。
2.3.7 互斥鎖函數:除了atomic和critical編譯制導指令,OpenMP還可以通過庫函數支持實現互斥操作,方便用戶實現特定的同步需求。編譯制導指令的互斥支持只能放置在一段代碼之前,作用在這段代碼之上。而OpenMP API所提供的互斥函數可放在任意需要的位置。程序員必須自己保證在調用相應鎖操作之后釋放相應的鎖,否則就可能造成多線程程序的死鎖。互斥鎖函數中只有omp_test_lock函數是帶有返回值的,該函數可以看作是omp_set_lock的非阻塞版本。
static omp_lock_t lock;int i;omp_init_lock(&lock);
#pragma omp parallel for
for (i=0; i<5; ++i) {omp_set_lock(&lock);printf("%d +\n", omp_get_thread_num());printf("%d -\n", omp_get_thread_num());omp_unset_lock(&lock);
}
omp_destroy_lock(&lock);
0 +
0 -
0 +
0 -
1 +
1 -
3 +
3 -
2 +
2 -
示例對for循環中的所有內容進行加鎖保護,同時只能有一個線程執行for循環中的內容。
2.4 數據環境控制:通常來說,OpenMP是建立在共享存儲結構的計算機之上,使用操作系統提供的線程作為并發執行的基礎,所以線程間的全局變量和靜態變量是共享的,而局部變量、自動變量是私有的。但是對OpenMP編程而言,缺省變量往往是共享變量,而不管它是不是全局靜態變量還是局部自動變量。也就是說OpenMP各個線程的變量是共享還是私有,是依據OpenMP自身的規則和相關的數據子句而定,而不是依據操作系統線程或進程上的變量特性而定。OpenMP的數據處理子句包括private、firstprivate、lastprivate、shared、default、reduction copyin和copyprivate.它與編譯制導指令parallel、for和sections相結合用來控制變量的作用范圍。它們控制數據變量,比如,哪些串行部分中的數據變量被傳遞到程序的并行部分以及如何傳送,哪些變量對所有并行部分的線程是可見的,哪些變量對所有并行部分的線程是私有的,等等。
2.4.1 共享與私有化:
2.4.1.1 shared子句:用來聲明一個或多個變量是共享變量。需要注意的是,在并行域內使用共享變量時,如果存在寫操作,必須對共享變量加以保護,否則不要輕易使用共享變量,盡量將共享變量的訪問轉化為私有變量的訪問。循環迭代變量在循環構造的任務分擔域里是私有的。聲明在任務分擔域內的自動變量都是私有的。
2.4.1.2 default子句:用來允許用戶控制并行區域中變量的共享屬性。使用shared時,缺省情況下,傳入并行區域內的同名變量被當作共享變量來處理,不會產生線程私有副本,除非使用private等子句來指定某些變量為私有的才會產生副本。如果使用none作為參數,除了那些由明確定義的除外,線程中用到的變量都必須顯式指定為是共享的還是私有的。
2.4.1.3 private子句:用來將一個或多個變量聲明成線程私有的變量,變量聲明成私有變量后,指定每個線程都有它自己的變量私有副本,其他線程無法訪問私有副本。即使在并行域外有同名的共享變量,共享變量在并行域內不起任何作用,并且并行域內不會操作到外面的共享變量。出現在reduction子句中的變量不能出現在private子句中。
int k = 100;#pragma omp parallel for private(k)
for (k=0; k<8; ++k) {printf("k=%d\n", k);
}
printf("last k=%d\n", k);
k=0
k=1
k=4
k=5
k=2
k=3
k=6
k=7
last k=100
for循環前的變量k和循環區域內的變量k其實是兩個不同的變量。用private子句聲明的私有變量的初始值在并行域的入口處是未定義的,它并不會繼承同名共享變量的值。
2.4.1.4 firstprivate子句:私有變量的初始化和終結操作,OpenMP編譯制導指令需要對這種需求給予支持,即使用firstprivate和lastprivate來滿足這兩種需求。使得并行域或任務分擔域開始執行時,私有變量通過主線程中的變量初始化,也可以在并行域或任務分擔結束時,將最后一次一個線程上的私有變量賦值給主線程的同名變量。private聲明的私有變量不會繼承同名變量的值,于是OpenMP提供了firstprivate子句來實現這個功能。firstprivate子句是private子句的超集,即不僅包含了private子句的功能,而且還要對變量做進行初始化。
int i, k=100;#pragma omp parallel for firstprivate(k)
for (i=0; i<4; ++i) {k += i;printf("k=%d\n", k);
}printf("last k=%d\n", k);
k=100
k=103
k=101
k=102
last k=100
并行域內的私有變量k繼承了外面共享變量k的值100作為初始值,并且在退出并行區域后,共享變量k的值保持為100未變。
2.4.1.5 lastprivate子句:有時要將任務分擔域內私有變量的值經過計算后,在退出時,將它的值賦給同名的共享變量(private和firstprivate子句在退出并行域時都沒有將私有變量的最后取值賦給對應的共享變量),lastprivate子句就是用來實現在退出并行域時將私有變量的值賦給共享變量。lastprivate子句也是private子句的超集,即不僅包含了private子句的功能,而且還要將變量從for、sections的任務分擔域中最后的線程中復制給外部同名變量。由于在并行域內是多個線程并行執行的,最后到底是將哪個線程的最終計算結果賦給了對應的共享變量呢?OpenMP規范中指出,如果是for循環迭代,那么是將最后一次循環迭代中的值賦給對應的共享變量;如果是sections構造,那么是代碼中排在最后的section語句中的值賦給對應的共享變量。注意這里說的最后一個section是指程序語法上的最后一個,而不是實際運行時的最后一個運行完的。
int i, 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=101
k=100
k=102
k=103
last k=103
退出for循環的并行區域后,共享變量k的值變成了103,而不是保持原來的100不變。
2.4.1.6 flush:OpenMP的flush制導指令主要與多個線程之間的共享變量的一致性問題。用法:flush[(list)],該指令將列表中的變量執行flush操作,直到所有變量都已完成相關操作后才返回,保證了后續變量訪問的一致性。
2.4.2 線程專有數據:它和私有數據不太相同,threadprivate子句用來指定全局的對象被各個線程各自復制了一個私有的拷貝,即各個線程具有各自私有、線程范圍內的全局對象。private變量在退出并行域后則失效,而threadprivate線程專有變量可以在前后多個并行域之間保持連續性。
2.4.2.1 threadprivate子句:用法:#pragma omp threadprivate(list) new-line。用作threadprivate的變量的地址不能是常數。對于C++的類(class)類型變量,用作threadprivate的參數時有些限制,當定義時帶有外部初始化則必須具有明確的拷貝構造函數。對于windows系統,threadprivate不能用于動態裝載(使用LoadLibrary裝載)的DLL中,可以用于靜態裝載的DLL中。
int counter = 0;#pragma omp threadprivate(counter)
int increment_counter()
{counter++;return (counter);
}
實現一個線程私有的計數器,各個線程使用同一個函數來實現自己的計數。
如果是靜態變量也同樣可以使用threadprivate聲明成線程私有的。
2.4.2.2 copyin子句:用來將主線程中threadprivate變量的值復制到執行并行域的各個線程的threadprivate變量中,便于所有線程訪問主線程中的變量值。copyin中的參數必須被聲明成threadprivate的,對于class類型的變量,必須帶有明確的拷貝賦值操作符。
int counter = 0;#pragma omp threadprivate(counter)
int increment_counter()
{counter++;return (counter);
}int main(int argc, char* argv[])
{int iterator;#pragma omp parallel sections copyin(counter){#pragma omp section{int count1;for (iterator=0; iterator<100; ++iterator) {count1 = increment_counter();}printf("count1=%ld\n", count1);}#pragma omp section{int count2;for (iterator=0; iterator<200; ++iterator) {count2 = increment_counter();}printf("count2=%ld\n", count2);}}printf("counter=%ld\n", counter);int iterator;#pragma omp parallel sections copyin(counter){#pragma omp section{int count1;for (iterator=0; iterator<100; ++iterator) {count1 = increment_counter();}printf("count1=%ld\n", count1);}#pragma omp section{int count2;for (iterator=0; iterator<200; ++iterator) {count2 = increment_counter();}printf("count2=%ld\n", count2);}}printf("counter=%ld\n", counter);return 0;
}
count1=100
count2=200
counter=0
threadprivate中的計數器函數,如果多個線程使用時,各個線程都需要對全局變量counter的副本進行初始化。
2.4.2.3 copyprivate子句:提供了一種機制,即將一個線程私有變量的值廣播到執行同一并行域的其他線程。copyprivate子句可以關聯single構造,在single構造的barrier到達之前就完成了廣播工作。copyprivate可以對private和threadprivate子句中的變量進行操作,但是當使用single構造時,copyprivate的變量不能用于private和firstprivate子句中。
int counter = 0;#pragma omp threadprivate(counter)
int increment_counter()
{counter++;return (counter);
}int main(int argc, char* argv[])
{#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);}return 0;
}
Threadid:0, count=51
Threadid:1, count=51
Threadid:3, count=51
Threadid:2, count=51
使用copyprivate子句后,single構造內給counter賦的值被廣播到了其它線程里,但沒有使用copyprivate子句時,只有一個線程獲得了single構造內的賦值,其它線程沒有獲取single構造內的賦值。
2.4.3 歸約操作:reduction子句主要用來對一個或多個參數條目指定一個操作符,每個線程將創建參數條目的一個私有拷貝,在并行域或任務分擔域的結束處,將用私有拷貝的值通過指定的運行符運算,原始的參數條目被運算結果的值更新。列出了可以用于reduction子句的一些操作符以及對應私有拷貝變量缺省的初始值,私有拷貝變量的實際初始值依賴于reduction變量的數據類型:+(0)、-(0)、*(1)、&(~0)、|(0)、^(0)、&&(1)、||(0)。如果在并行域內不加鎖保護就直接對共享變量進行寫操作,存在數據競爭問題,會導致不可預測的異常結果。如果共享數據作為private、firstprivate、lastprivate、threadprivate、reduction子句的參數進入并行域后,就變成線程私有了,不需要加鎖保護了。
int i, sum = 100;#pragma omp parallel for reduction(+:sum)
for (i=0; i<1000; ++i) {sum += i;
}printf("sum=%ld\n", sum);
sum=499600
OpenMP編譯器結構:它和所有編譯器一樣具有相似的結構,即典型的八個部件構成,它們分別是詞法分析、語法分析、語義分析、中間代碼生成、代碼優化、目標代碼生成、信息表管理和錯誤處理。(1)、詞法分析程序:詞法分析(Lexical analysis)或掃描(Scanning)是編譯器最前端的輸入模塊,它將源代碼文件中的一個長長的字符串,逐個識別為有意義的詞素(Lexeme)或單詞符號,并轉變為便于內部處理的格式來保存。通常詞法掃碼器的工作任務有:識別出源程序中的每個基本語法單位(通常稱為單詞或語法符號);刪除無用的空白字符、回車字符以及其他與語言無直接關系的非實質性字符;刪除注釋行;進行詞法檢查并報告所發現的錯誤。(2)、語法分析程序:語法分析(Syntax Analysis)或解析(Parsing)程序需要借助于詞法分析,將詞法分析輸出的內部編碼格式表示的單詞序列嘗試構建出一個符合語法規則的完整語法樹。如果無法成功建立起一個合法的語法樹,則由錯誤處理模塊輸出相應的語法錯誤信息。(3)、語義分析程序(Semantic Analysis):語義特征表征的是各個語法成分的含義和功能,包括這些語法元素的屬性或執行時應進行的運算或操作。(4)、中間代碼生成:指的是編譯器未輸出目標代碼之前在內部使用的一種源代碼的等價表示。(5)、代碼優化程序:代碼優化是為了提供更高質量目標代碼,該工作常常在中間代碼生成和目標代碼輸出之間插入一個代碼優化處理的階段來實現。根據目標代碼的目標期望不同,優化方法也相應不同,有的是以運行時間為標準越快越好,有的是以存儲空間開銷為標準占用內存越少越好。(6)、目標代碼生成:是以語義分析(也可能加上優化處理)產生的中間代碼作為輸入的,它將中間代碼翻譯為最終形式的目標代碼。(7)、信息表管理程序:在編譯過程中總是需要收集、記錄或查詢源程序中出現的各種量的有關屬性信息,因此編譯程序需要建立和維護多個不同用途的表格(例如常數表、變量名、循環層次等等),這些表格統稱為符號表。在編譯過程中,造表和查表工作由一系列程序(或函數)來完成,它們并不是獨立的存在而是安插在編譯程序的相關功能代碼中。(8)、錯誤處理:由于編程人員不可避免的會寫出有錯誤的代碼,一個可用的編譯器必須能夠發現大多數常見錯誤,并能準確地報告出錯誤在源代碼中的位置,否則就沒有使用價值。
GitHub:OpenMP test code
總結
以上是生活随笔為你收集整理的《OpenMP编译原理及实现技术》摘录的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VS2010下编译OpenCV2.4.6
- 下一篇: OpenMP知识点汇总