OpenMP並行程式設計(二)
1、fork/join並存執行模式的概念
2、OpenMP指令和庫函數介紹
3、parallel 指令的用法
4、for指令的使用方法
5 sections和section指令的用法
1、fork/join並存執行模式的概念
OpenMP是一個編譯器指令和庫函數的集合,主要是為共用式儲存電腦上的並行程式設計使用的。
前面一篇文章中已經試用了OpenMP的一個Parallel for指令。從上篇文章中我們也可以發現OpenMP並存執行的程式要全部結束後才能執行後面的非並行部分的代碼。這就是標準的並行模式fork/join式並行模式,共用儲存式並行程式就是使用fork/join式並行的。
標準並行模式執行代碼的基本思想是,程式開始時只有一個主線程,程式中的串列部分都由主線程執行,並行的部分是通過派生其他線程來執行,但是如果並行部分沒有結束時是不會執行串列部分的,如上一篇文章中的以下代碼:
int main(int argc, char* argv[])
{
clock_t t1 = clock();
#pragma omp parallel for
for ( int j = 0; j < 2; j++ ){
test();
}
clock_t t2 = clock();
printf("Total time = %d\n", t2-t1);
test();
return 0;
}
在沒有執行完for迴圈中的代碼之前,後面的clock_t t2 = clock();這行代碼是不會執行的,如果和調用線程建立函數相比,它相當於先建立線程,並等待線程執行完,所以這種並行模式中在主線程裡建立的線程並沒有和主線程並行運行。
2、OpenMP指令和庫函數介紹
下面來介紹OpenMP的基本指令和常用指令的用法,
在C/C++中,OpenMP指令使用的格式為
#pragma omp 指令 [子句[子句]…]
前面提到的parallel for就是一條指令,有些書中也將OpenMP的“指令”叫做“編譯指導語句”,後面的子句是可選的。例如:
#pragma omp parallel private(i, j)
parallel 就是指令, private是子句
為敘述方便把包含#pragma和OpenMP指令的一行叫做語句,如上面那行叫parallel語句。
OpenMP的指令有以下一些:
parallel,用在一個程式碼片段之前,表示這段代碼將被多個線程並存執行
for,用於for迴圈之前,將迴圈分配到多個線程中並存執行,必須保證每次迴圈之間無相關性。
parallel for, parallel 和 for語句的結合,也是用在一個for迴圈之前,表示for迴圈的代碼將被多個線程並存執行。
sections,用在可能會被並存執行的程式碼片段之前
parallel sections,parallel和sections兩個語句的結合
critical,用在一段代碼臨界區之前
single,用在一段只被單個線程執行的程式碼片段之前,表示後面的程式碼片段將被單線程執行。
flush,
barrier,用於並行區內代碼的線程同步,所有線程執行到barrier時要停止,直到所有線程都執行到barrier時才繼續往下執行。
atomic,用於指定一塊記憶體地區被制動更新
master,用於指定一段代碼塊由主線程執行
ordered, 用於指定並列區域的迴圈按順序執行
threadprivate, 用於指定一個變數是線程私人的。
OpenMP除上述指令外,還有一些庫函數,下面列出幾個常用的庫函數:
omp_get_num_procs, 返回運行本線程的多處理機的處理器個數。
omp_get_num_threads, 返回當前並列區域中的活動線程個數。
omp_get_thread_num, 返回線程號
omp_set_num_threads, 設定並存執行代碼時的線程個數
omp_init_lock, 初始化一個簡單鎖
omp_set_lock, 上鎖操作
omp_unset_lock, 解鎖操作,要和omp_set_lock函數配對使用。
omp_destroy_lock, omp_init_lock函數的配對操作函數,關閉一個鎖
OpenMP的子句有以下一些
private, 指定每個線程都有它自己的變數私人副本。
firstprivate,指定每個線程都有它自己的變數私人副本,並且變數要被繼承主線程中的初值。
lastprivate,主要是用來指定將線程中的私人變數的值在平行處理結束後複製回主線程中的對應變數。
reduce,用來指定一個或多個變數是私人的,並且在平行處理結束後這些變數要執行指定的運算。
nowait,忽略指定中暗含的等待
num_threads,指定線程的個數
schedule,指定如何調度for迴圈迭代
shared,指定一個或多個變數為多個線程間的共用變數
ordered,用來指定for迴圈的執行要按順序執行
copyprivate,用於single指令中的指定變數為多個線程的共用變數
copyin,用來指定一個threadprivate的變數的值要用主線程的值進行初始化。
default,用來指定平行處理地區內的變數的使用方式,預設是shared
3、parallel 指令的用法
parallel 是用來構造一個並行塊的,也可以使用其他指令如for、sections等和它配合使用。
在C/C++中,parallel的使用方法如下:
#pragma omp parallel [for | sections] [子句[子句]…]
{
//代碼
}
parallel語句後面要跟一個大括弧對將要並存執行的代碼括起來。
void main(int argc, char *argv[]) {
#pragma omp parallel
{
printf(“Hello, World!\n”);
}
}
執行以上代碼將會列印出以下結果
Hello, World!
Hello, World!
Hello, World!
Hello, World!
可以看得出parallel語句中的代碼被執行了四次,說明總共建立了4個線程去執行parallel語句中的代碼。
也可以指定使用多少個線程來執行,需要使用num_threads子句:
void main(int argc, char *argv[]) {
#pragma omp parallel num_threads(8)
{
printf(“Hello, World!, ThreadId=%d\n”, omp_get_thread_num() );
}
}
執行以上代碼,將會列印出以下結果:
Hello, World!, ThreadId = 2
Hello, World!, ThreadId = 6
Hello, World!, ThreadId = 4
Hello, World!, ThreadId = 0
Hello, World!, ThreadId = 5
Hello, World!, ThreadId = 7
Hello, World!, ThreadId = 1
Hello, World!, ThreadId = 3
從ThreadId的不同可以看出建立了8個線程來執行以上代碼。所以parallel指令是用來為一段代碼建立多個線程來執行它的。parallel塊中的每行代碼都被多個線程重複執行。
和傳統的建立線程函數比起來,相當於為一個線程入口函數重複調用建立線程函數來建立線程並等待線程執行完。
4、for指令的使用方法
for指令則是用來將一個for迴圈分配到多個線程中執行。for指令一般可以和parallel指令合起來形成parallel for指令使用,也可以單獨用在parallel語句的並行塊中。
#pragma omp [parallel] for [子句]
for迴圈語句
先看看單獨使用for語句時是什麼效果:
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
從結果可以看出四次迴圈都在一個線程裡執行,可見for指令要和parallel指令結合起來使用才有效果:
如以下代碼就是parallel 和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 = 2, ThreadId = 2
j = 1, ThreadId = 1
j = 3, ThreadId = 3
可見迴圈被分配到四個不同的線程中執行。
上面這段代碼也可以改寫成以下形式:
int j = 0;
#pragma omp parallel
{
#pragma omp for
for ( j = 0; j < 4; j++ ){
printf(“j = %d, ThreadId = %d\n”, j, omp_get_thread_num());
}
}
執行以上代碼會列印出以下結果:
j = 1, ThreadId = 1
j = 3, ThreadId = 3
j = 2, ThreadId = 2
j = 0, ThreadId = 0
在一個parallel 塊中也可以有多個for語句,如:
int j;
#pragma omp parallel
{
#pragma omp for
for ( j = 0; j < 100; j++ ){
…
}
#pragma omp for
for ( j = 0; j < 100; j++ ){
…
}
…
}
for 迴圈語句中,書寫是需要按照一定規範來寫才可以的,即for迴圈小括弧內的語句要按照一定的規範進行書寫,for語句小括弧裡共有三條語句
for( i=start; i < end; i++)
i=start; 是for迴圈裡的第一條語句,必須寫成 “變數=初值” 的方式。如 i=0
i < end;是for迴圈裡的第二條語句,這個語句裡可以寫成以下4種形式之一:
變數 < 邊界值
變數 <= 邊界值
變數 > 邊界值
變數 >= 邊界值
如 i>10 i< 10 i>=10 i>10 等等
最後一條語句i++可以有以下9種寫法之一
i++
++i
i--
--i
i += inc
i -= inc
i = i + inc
i = inc + i
i = i –inc
例如i += 2; i -= 2;i = i + 2;i = i - 2;都是符合規範的寫法。
5 sections和section指令的用法
section語句是用在sections語句裡用來將sections語句裡的代碼劃分成幾個不同的段,每段都並存執行。用法如下:
#pragma omp [parallel] sections [子句]
{
#pragma omp section
{
代碼塊
}
}
先看一下以下的例子代碼:
void main(int argc, char *argv)
{
#pragma omp parallel sections {
#pragma omp section
printf(“section 1 ThreadId = %d\n”, omp_get_thread_num());
#pragma omp section
printf(“section 2 ThreadId = %d\n”, omp_get_thread_num());
#pragma omp section
printf(“section 3 ThreadId = %d\n”, omp_get_thread_num());
#pragma omp section
printf(“section 4 ThreadId = %d\n”, omp_get_thread_num());
}
執行後將列印出以下結果:
section 1 ThreadId = 0
section 2 ThreadId = 2
section 4 ThreadId = 3
section 3 ThreadId = 1
從結果中可以發現第4段代碼執行比第3段代碼早,說明各個section裡的代碼都是並存執行的,並且各個section被分配到不同的線程執行。
使用section語句時,需要注意的是這種方式需要保證各個section裡的代碼執行時間相差不大,否則某個section執行時間比其他section過長就達不到並存執行的效果了。
上面的代碼也可以改寫成以下形式:
void main(int argc, char *argv)
{
#pragma omp parallel {
#pragma omp sections
{
#pragma omp section
printf(“section 1 ThreadId = %d\n”, omp_get_thread_num());
#pragma omp section
printf(“section 2 ThreadId = %d\n”, omp_get_thread_num());
}
#pragma omp sections
{
#pragma omp section
printf(“section 3 ThreadId = %d\n”, omp_get_thread_num());
#pragma omp section
printf(“section 4 ThreadId = %d\n”, omp_get_thread_num());
}
}
執行後將列印出以下結果:
section 1 ThreadId = 0
section 2 ThreadId = 3
section 3 ThreadId = 3
section 4 ThreadId = 1
這種方式和前面那種方式的區別是,兩個sections語句是串列執行的,即第二個sections語句裡的代碼要等第一個sections語句裡的代碼執行完後才能執行。
用for語句來分攤是由系統自動進行,只要每次迴圈間沒有時間上的差距,那麼分攤是很均勻的,使用section來劃分線程是一種手工劃分線程的方式,最終並行性的好壞得依賴於程式員。
本篇文章中講的幾個OpenMP指令parallel, for, sections, section實際上都是用來如何建立線程的,這種建立線程的方式比起傳統調用建立線程函數建立線程要更方便,並且更高效。
當然,建立線程後,線程裡的變數是共用的還是其他方式,主線程中定義的變數到了並行塊內後還是和傳統建立線程那種方式一樣的嗎?建立的線程是如何調度的?等等諸如此類的問題到下一篇文章中進行講解。