private/firstprivate/lastprivate/threadprivate,首先要知道的是,它們分為兩大類,一類是private/firstprivate/lastprivate子句,另一類是threadprivate,為指令。(PS:有些地方把threadprivate說成是子句,但是實際來講,它是一個指令。)
可以參考http://blog.csdn.net/gengshenghong/article/details/6970220可以查看哪些指令能接受哪些子句。
(1) private
private子句將一個或多個變數聲明為線程的私人變數。每個線程都有它自己的變數私人副本,其他線程無法訪問。即使在並列區域外有同名的共用變數,共用變數在並列區域內不起任何作用,並且並列區域內不會操作到外面的共用變數。
注意:
1. private variables are undefined on entry and exit of the parallel region.即private變數在進入和退出並列區域是“未定義“的。
2. The value of the original variable (before the parallel region) is undefined after the parallel region!在並列區域之前定義的原來的變數,在並列區域後也是”未定義“的。
3. A private variable within the parallel region has no storage association with the same variable outside of the region. 並列區域內的private變數和並列區域外同名的變數沒有儲存關聯。
說明:private的很容易理解錯誤。下面用例子來說明上面的注意事項,
A. private變數在進入和退出並列區域是”未定義“的。
int main(int argc, _TCHAR* argv[]){int A=100;#pragma omp parallel for private(A)for(int i = 0; i<10;i++){printf("%d\n",A);}return 0;}
初學OpenMP很容易認為這段代碼是沒有問題的。其實,這裡的A在進入並列區域的時候是未定義的,所以在並列區域直接對其進行讀操作,會導致執行階段錯誤。
其實,在VS中編譯這段代碼,就會有編譯警告:
warning C4700: uninitialized local variable 'A' used
很清楚的指向"printf"這句,A是沒有初始化的變數。所以,運行時候會出現運行時崩潰的錯誤。
這段代碼能說明,private在進入並列區域是未定義的,至於退出並列區域就不容易舉例說明了,本身,這裡的三個注意事項是交叉理解的,說明的是一個含義,所以,看下面的例子來理解。
B. 在並列區域之前定義的原來的變數,在並列區域後也是”未定義“的。
int main(int argc, _TCHAR* argv[]){int B;#pragma omp parallel for private(B)for(int i = 0; i<10;i++){B = 100; }printf("%d\n",B);return 0;}
這裡的B在並列區域內進行了賦值等操作,但是在退出並列區域後,是未定義的。理解”在並列區域之前定義的原來的變數,在並列區域後也是”未定義“的“這句話的時候,要注意,不是說所有的在並列區域內定義的原來的變數,使用了private子句後,退出並列區域後就一定是未定義的,如果原來的變數,本身已經初始化,那麼,退出後,不會處於未定義的狀態,就是下面的第三個注意事項要說明的問題。
C. 並列區域內的private變數和並列區域外同名的變數沒有儲存關聯
int main(int argc, _TCHAR* argv[]){int C = 100;#pragma omp parallel for private(C)for(int i = 0; i<10;i++){C = 200; printf("%d\n",C);}printf("%d\n",C);return 0;}
這裡,在退出並列區域後,printf的C的結果是100,和並列區域內對其的操作無關。
總結來說,上面的三點是交叉的,第三點包含了所有的情況。所以,private的關鍵理解是:A private variable within the parallel region has no storage association with the same variable outside of the region. 簡單點理解,可以認為,並列區域內的private變數和並列區域外的變數沒有任何關聯。如果非要說點關聯就是,在使用private的時候,在之前要先定義一下這個變數,但是,到了並列區域後,並列區域的每個線程會產生此變數的副本,而且是沒有初始化的。
下面是綜合上面的例子,參考注釋的解釋:
int main(int argc, _TCHAR* argv[]){int A=100,B,C=0;#pragma omp parallel for private(A) private(B)for(int i = 0; i<10;i++){B = A + i;// A is undefined! Runtime error!printf("%d\n",i);}/*--End of OpemMP paralle region. --*/C = B;// B is undefined outside of the parallel region!printf("A:%d\n", A);printf("B:%d\n", B);return 0;}
(2)firstprivate
Private子句的私人變數不能繼承同名變數的值,firstprivate則用於實現這一功能-繼承並列區域額之外的變數的值,用於在進入並列區域之前進行一次初始化。
Firstprivate(list):All variables in the list areinitialized with the value the original object had before entering the parallelconstruct.
分析下面的例子:
int main(int argc, _TCHAR* argv[]){int A;#pragma omp parallel for firstprivate(A)for(int i = 0; i<10;i++){printf("%d: %d\n",i, A);// #1}printf("%d\n",A);// #2return 0;}
用VS編譯發現,也會報一個“warning C4700: uninitialized local variable 'A' used”的警告,但是這裡其實兩個地方用到了A。實際上,這個警告是針對第二處的,可以看出,VS並沒有給第一處OpenMP並列區域內的A有警告,這是由於使用firstprivate的時候,會對並列區域內的A使用其外的同名共用變數就行初始化,當然,如果嚴格分析,外面的變數其實也是沒有初始化的,理論上也是可以認為應該警示告,但是,具體而言,這是跟VS的實現有關的,另外,在debug下,上面的程式會崩潰,release下,其實是可以輸出值的,總之,上面的輸出是無法預料的。
再看下面的例子,和前面private的例子很類似:
int main(int argc, _TCHAR* argv[]){int A = 100;#pragma omp parallel for firstprivate(A)for(int i = 0; i<10;i++){printf("%d: %d\n",i, A);// #1}printf("%d\n",A);// #2return 0;}
這裡,如果使用private,那麼並列區域內是有問題的,因為並列區域內的A是沒有初始化的,導致無法預料的輸出或崩潰。但是,使用了firstprivate後,這樣,進入並列區域的時候,每一個線程的A的副本都會利用並列區域外的同名共用變數A的值進行一次初始化,所以,輸出的A都是100.
繼續探討這裡的“進行一次初始化”,為了理解“一次”的含義,看下面的例子:
#include <omp.h>int main(int argc, _TCHAR* argv[]){int A = 100;#pragma omp parallel for firstprivate(A)for(int i = 0; i<10;i++){printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);// #1A = i;}printf("%d\n",A);// #2return 0;}
這裡,每次輸出後,改變A的值,需要注意的是,這裡的“進行一次初始化”是針對team內的每一個線程進行一次初始化,對於上面的程式,在4核的CPU上運行,並列區域內有四個線程,所以每一個線程都會有A的一個副本,因而,上面的程式輸出結果可能如下:
其實,這個結果是很容易理解的,不可能是每一個for都有一個變數的副本,而是每一個線程,所以這個結果在預料之中。
仍然藉助上面這個例子,協助理解private和firstprivate,從而引出lastprivate,private對於並列區域的每一個線程都有一個副本,並且和並列區域外的變數沒有關聯;firstprivate解決了進入並行區的問題,即在進入並列區域的每個線程的副本變數使用並列區域外的共用變數進行一個初始化的工作,那麼下面有一個問題就是,如果希望並列區域的副本變數,在退出並行區的時候,能反過來賦值給並列區域外的共用變數,那麼就需要依靠lastprivate了。
(3)lastprivate
如果需要在並列區域內的私人變數經過計算後,在退出並列區域時,需要將其值賦給同名的共用變數,就可以使用lastprivate完成。
Lastprivate(list):The thread that executes the sequentially last iteration or section updates thevalue of the objects in the list.
從上面的firstprivate的最後一個例子可以看出,並列區域對A進行了賦值,但是退出並列區域後,其值仍然為原來的值。
這裡首先有一個問題是:退出並列區域後,需要將並列區域內的副本的值賦值為同名的共用變數,那麼,並列區域內有多個線程,是哪一個線程的副本用於賦值呢?
是否是最後一個運行完畢的線程?否!OpenMP規範中指出,如果是迴圈迭代,那麼是將最後一次迴圈迭代中的值賦給對應的共用變數;如果是section構造,那麼是最後一個section語句中的值賦給對應的共用變數。注意這裡說的最後一個section是指程式文法上的最後一個,而不是實際運行時的最後一個運行完的。
在理解這句話之前,先利用一個簡單的例子來理解一下lastprivate的作用:
int main(int argc, _TCHAR* argv[]){int A = 100;#pragma omp parallel for lastprivate(A)for(int i = 0; i<10;i++){A = 10;}printf("%d\n",A);return 0;}
這裡,很容易知道結果為10,而不是100.這就是lastprivate帶來的效果,退出後會有一個賦值的過程。
理解了lastprivate的基本含義,就可以繼續來理解上面的紅色文字部分的描述了,即到底是哪一個線程的副本用於對並列區域外的變數賦值的問題,下面的例子和前面firstprivate的例子很類似:
#include <omp.h>int main(int argc, _TCHAR* argv[]){int A = 100;#pragma omp parallel for lastprivate(A)for(int i = 0; i<10;i++){printf("Thread ID: %d, %d\n",omp_get_thread_num(), i);// #1A = i;}printf("%d\n",A);// #2return 0;}
從結果可以看出,最後並列區域外的共用變數的值並不是最後一個線程退出的值,多次運行發現,並列區域的輸出結果可能發生變化,但是最終的輸出都是9,這就是上面的OpenMP規範說明的問題,退出並列區域的時候,是根據“邏輯上”的最後一個線程用於對共用變數賦值,而不是實際啟動並執行最後一個線程,對於for而言,就是最後一個迴圈迭代所線上程的副本值,用於對共用變數賦值。
另外,firstprivate和lastprivate分別是利用共用變數對線程副本初始化(進入)以及利用線程副本對共用變數賦值(退出),private是線程副本和共用變數無任何關聯,那麼如果希望進入的時候初始化並且退出的時候賦值呢?事實上,可以對同一個變數使用firstprivate和lastprivate的,下面的例子即可看出:
#include <omp.h>int main(int argc, _TCHAR* argv[]){int A = 100;#pragma omp parallel for firstprivate(A) lastprivate(A)for(int i = 0; i<10;i++){printf("Thread ID: %d, %d: %d\n",omp_get_thread_num(), i, A);// #1A = i;}printf("%d\n",A);// #2return 0;}
說明:不能對一個變數同時使用兩次private,或者同時使用private和firstprivate/lastprivate,只能firstprivate和lastprivate一起使用。
關於lastprivate,還需要說明的一點是,如果是類(class)類型的變數使用在lastprivate參數中,那麼使用時有些限制,需要一個可訪問的,明確的預設建構函式,除非變數也被使用作為firstprivate子句的參數;還需要一個拷貝賦值操作符,並且這個拷貝賦值操作符對於不同對象的操作順序是未指定的,依賴於編譯器的定義。
另外,firstprivate和private可以用於所有的並行構造塊,但是lastprivate只能用於for和section組成的並行塊之中,參考http://blog.csdn.net/gengshenghong/article/details/6970220的對照表。
(4)threadprivate
首先,threadprivate和上面幾個子句的區別在於,threadprivate是指令,不是子句。threadprivate指定全域變數被OpenMP所有的線程各自產生一個私人的拷貝,即各個線程都有自己私人的全域變數。一個很明顯的區別在於,threadprivate並不是針對某一個並列區域,而是整個於整個程式,所以,其拷貝的副本變數也是全域的,即在不同的並列區域之間的同一個線程也是共用的。
threadprivate只能用於全域變數或靜態變數,這是很容易理解的,根據其功能。
根據下面的例子,來進一步理解threadprivate的使用:
#include <omp.h>int A = 100;#pragma omp threadprivate(A)int main(int argc, _TCHAR* argv[]){#pragma omp parallel forfor(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#pragma omp parallel forfor(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);// #2return 0;}
分析結果,發現,第二個並列區域是在第一個並列區域的基礎上繼續遞增的;每一個線程都有自己的全域私人變數。另外,觀察在並列區域外的列印的“Globa A”的值可以看出,這個值總是前面的thread 0的結果,這也是預料之中的,因為退出並列區域後,只有master線程運行。
threadprivate指令也有自己的一些子句,就不在此分析了。另外,如果使用的是C++的類,對於類的建構函式也會有類似於lastprivate的一些限制。
總結:
private/firstprivate/lastprivate都是子句,用於表示並列區域內的變數的資料範圍屬性。其中,private表示並列區域team內的每一個線程都會產生一個並列區域外同名變數的共用變數,且和共用變數沒有任何關聯;firstprivaet在private的基礎上,在進入並列區域時(或說每個線程建立時,或副本變數構造時),會使用並列區域外的共用變數進行一次初始化工作;lastprivate在private的基礎上,在退出並列區域時,會使用並列區域內的副本的變數,對共用變數進行賦值,由於有多個副本,OpenMP規定了如何確定使用哪個副本進行賦值。另外,private不能和firstprivate/lastprivate混用於同一個變數,firstprivate和lastprivate可以對同一變數使用,效果為兩者的結合。
threadprivate是指令,和private的區別在於,private是針對並列區域內的變數的,而threadprivate是針對全域的變數的。