在上一篇文章中介紹了並行計算的基礎概念,也順便介紹了OpenMP。
OpenMp提供了對於並行描述的高層抽象,降低了並行編程的難度和復雜度,這樣程序員可以把更多的精力投入到並行算法本身,而非其具體實現細節。對基於數據分集的多線程程序設計,OpenMP是一個很好的選擇。同時,使用OpenMP也提供了更強的靈活性,可以較容易的適應不同的並行系統配置。線程粒度和負載平衡等是傳統多線程程序設計中的難題,但在OpenMp中,OpenMp庫從程序員手中接管了部分這兩方面的工作。但是,作為高層抽象,OpenMp並不適合需要復雜的線程間同步和互斥的場合。OpenMp的另一個缺點是不能在非共享內存系統(如計算機集群)上使用,一般在這樣的系統上,MPI使用較多。
在Visual Studio中使用OpenMP其實很簡單,只要將 Project 的Properties中C/C++里Language的OpenMP Support開啟(參數為 /openmp),就可以讓VC++在編譯時就可以支持OpenMP 的語法了。而在編寫使用OpenMP 的程序時,添加#include <omp.h>即可。下面是一個實例:

#include <stdio.h> #include <omp.h> #include <windows.h> #define MAX_VALUE 10000000 double _test(int value) { int index = 0; double result = 0.0; for(index = value + 1; index < MAX_VALUE; index +=2 ) result += 1.0 / index; return result; } void OpenMPTest() { int index= 0; int time1 = 0; int time2 = 0; double value1 = 0.0, value2 = 0.0; double result[2]; time1 = GetTickCount(); for(index = 1; index < MAX_VALUE; index ++) value1 += 1.0 / index; time1 = GetTickCount() - time1; memset(result , 0, sizeof(double) * 2); time2 = GetTickCount(); #pragma omp parallel for for(index = 0; index < 2; index++) result[index] = _test(index); value2 = result[0] + result[1]; time2 = GetTickCount() - time2; printf("time1 = %d,time2 = %d\n",time1,time2); return; } int main() { OpenMPTest(); system("pause"); return 0; }
在這里例子中用到了一個關鍵的語句:
#pragma omp parallel for
這個句子代表了C++中使用OpenMP的基本語法規則:#pragma omp 指令 [子句[子句]…]
1. OpenMP指令與庫函數
OpenMP包括以下指令:
- parallel:用在一個代碼段之前,表示這段代碼將被多個線程並行執行
- for:用於for循環之前,將循環分配到多個線程中並行執行,必須保證每次循環之間無相關性
- parallel for:parallel 和 for語句的結合,也是用在一個for循環之前,表示for循環的代碼將被多個線程並行執行
- sections:用在可能會被並行執行的代碼段之前
- parallel sections:parallel和sections兩個語句的結合
- critical:用在一段代碼臨界區之前
- single:用在一段只被單個線程執行的代碼段之前,表示后面的代碼段將被單線程執行
- 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
2. parallel指令用法
parallel 是用來構造一個並行塊的,也可以使用其他指令如for、sections等和它配合使用。其用法如下:
#pragma omp parallel [for | sections] [子句[子句]…] { // 需要並行執行的代碼 }
例如,可以寫一個簡單的並行輸出提示信息的代碼:
#pragma omp parallel num_threads(8) { printf(“Hello, World!, ThreadId=%d\n”, omp_get_thread_num() ); }
在本機測試將會得到如下結果:
結果表明,printf函數被創建了8個線程來執行,並且每一個線程執行的先后次序並不確定。和傳統的創建線程函數比起來,OpenMP相當於為一個線程入口函數重復調用創建線程函數來創建線程並等待線程執行完。如果在上面的代碼中去掉num_threads(8)來指定線程數目,那么將根據實際CPU核心數目來創建線程數。
3. for指令用法
for指令則是用來將一個for循環分配到多個線程中執行。for指令一般可以和parallel指令合起來形成parallel for指令使用,也可以單獨用在parallel語句的並行塊中。其語法如下:
#pragma omp [parallel] for [子句] for循環語句
例如有這樣一個例子:
#pragma omp parallel for for ( int j = 0; j < 4; j++ ) { printf("j = %d, ThreadId = %d\n", j, omp_get_thread_num()); }
可以得到如下結果:
從結果可以看出,for循環的語句被分配到不同的線程中分開執行了。需要注意的是,如果不添加parallel關鍵字,那么四次循環將會在同一個線程里執行,結果將會是下面這樣的:
4. sections和section的用法
section語句是用在sections語句里用來將sections語句里的代碼划分成幾個不同的段,每段都並行執行。用法如下:
#pragma omp [parallel] sections [子句] { #pragma omp section { // 代碼塊 } }
例如有這樣一個例子:
#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內部的代碼都是(分配到不同的線程中)並行執行的。使用section語句時,需要注意的是這種方式需要保證各個section里的代碼執行時間相差不大,否則某個section執行時間比其他section長太多就達不到並行執行的效果了。
如果將上面的代碼拆分成兩個sections,即:
#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 parallel 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()); }
產生的結果將會是這樣的:
可以看出,兩個sections之間是串行執行的,而section內部則是並行執行的。
小節:
用for語句來分攤任務是由系統自動進行的,只要每次循環間沒有時間上的差距,那么分攤是很均勻的,使用section來划分線程是一種手工划分線程的方式,最終並行性的好壞依賴於程序員。
本篇文章中講的幾個OpenMP指令parallel, for, sections, section實際上都是用來如何創建線程的,這種創建線程的方式比起傳統調用創建線程函數創建線程要更方便,並且更高效。