OpenMP

OpenMP（Open Multi-Processing）是一套支持跨平台共享內存方式的多線程並發的編程API，使用C,C++和Fortran語言，可以在大多數的處理器體系和作業系統中運行，包括Solaris, AIX, HP-UX, GNU/Linux, Mac OS X, 和Microsoft Windows。包括一套編譯器指令、庫和一些能夠影響運行行為的環境變量。

OpenMP採用可移植的、可擴展的模型，為程式設計師提供了一個簡單而靈活的開發平台，從標準桌面電腦到超級計算機的並行應用程式接口。

混合併行編程模型構建的應用程式可以同時使用OpenMP和MPI，或更透明地通過使用OpenMP擴展的非共享內存系統上運行的電腦集群。

OpenMP是由OpenMP Architecture Review Board牽頭提出的，並已被廣泛接受的，用於共享內存並行系統的多線程程序設計的一套指導性注釋（Compiler Directive）。OpenMP支持的程式語言包括C語言、C++和Fortran；而支持OpenMP的編譯器包括Sun Studio和Intel Compiler，以及開放源碼的GCC、LLVM和Open64編譯器。OpenMP提供了對並行演算法的高層的抽象描述，程式設計師通過在原始碼中加入專用的pragma來指明自己的意圖，由此編譯器可以自動將程序進行並行化，並在必要之處加入同步互斥以及通信。當選擇忽略這些pragma，或者編譯器不支持OpenMP時，程序又可退化為通常的程序（一般為串行），程式碼仍然可以正常運作，只是不能利用多線程來加速程序執行。

## 介紹

<image>

OpenMP是一個跨平台的多線程實現，主線程(順序的執行指令)生成一系列的子線程，並將任務劃分給這些子線程進行執行。這些子線程並行的運行，由運行時環境將線程分配給不同的處理器。

要進行並行執行的代碼片段需要進行相應的標記，用預編譯指令使得在代碼片段被執行前生成線程，每個線程會分配一個id，可以通過函數(called `omp_get_thread_num()`)來獲得該值，該值是一個整數，主線程的id為0。在並行化的代碼運行結束後，子線程join到主線程中，並繼續執行程序。

默認情況下，各個線程獨立地執行並行區域的代碼。可以使用Work-sharing constructs來劃分任務，使每個線程執行其分配部分的代碼。通過這種方式，使用OpenMP可以實現任務並行和數據並行。

運行時環境分配給每個處理器的線程數取決於使用方法、機器負載和其他因素。線程的數目可以通過環境變量或者代碼中的函數來指定。在C/C++中，OpenMP的函數都聲明在頭文件omp.h中。

## 歷史

OpenMP Architecture Review Board (ARB)於1997年10月發布了OpenMP for Fortran 1.0。次年的10月，發布了C/C++的標準。2000年，發布了Fortran語言的2.0版本，並於2002年發布了C/C++語言的2.0版本。2005年，包含Fortran和C/C++的2.5版本發布了。

在2008年5月發布了3.0版。3.0中的新功能包括任務(tasks)和任務結構(task construct)的概念。這些新功能總結在OpenMP3.0規範的附錄F中。

OpenMP規範的3.1版於2011年7月9日發布。

4.0版本在2013年7月發布，它增加或改進以下功能：支持加速器，原子性，錯誤處理，線程關聯，任務擴展，減少使用者定義的SIMD支持和Fortran 2003的支持。

## 特色

OpenMP提供的這種對於並行描述的高層抽象降低了並行編程的難度和複雜度，這樣程式設計師可以把更多的精力投入到並行演算法本身，而非其具體實現細節。對基於數據分集的多線程程序設計，OpenMP是一個很好的選擇。同時，使用OpenMP也提供了更強的靈活性，可以較容易的適應不同的並行系統配置。線程粒度和負載平衡等是傳統多線程程序設計中的難題，但在OpenMP中，OpenMP庫從程式設計師手中接管了部分這兩方面的工作。

## 語法

```
#pragma omp <directive> [clause[[,] clause] ...]
```

### directive

其中，directive共11個：

- atomic 記憶體位置將會原子更新（Specifies that a memory location that will be updated atomically.）
- barrier 線程在此等待，直到所有的線程都執行到此barrier。用來同步所有線程。
- critical 其後的代碼塊為臨界區，任意時刻只能被一個線程執行。
- flush 所有線程對所有共享對象具有相同的記憶體視圖（view of memory）
- for 用在for循環之前，把for循環並行化由多個線程執行。循環變量只能是整型
- master 指定由主線程來執行接下來的程式。
- ordered 指定在接下來的代碼塊中，被並行化的 for循環將依序執行（sequential loop）
- parallel 代表接下來的代碼塊將被多個線程並行各執行一遍。
- sections 將接下來的代碼塊包含將被並行執行的section塊。
- single 之後的程式將只會在一個線程（未必是主線程）中被執行，不會被並行執行。
- threadprivate 指定一個變量是線程局部存儲（thread local storage）

### clause

共計13個clause：

- copyin 讓threadprivate的變量的值和主線程的值相同。
- copyprivate 不同線程中的變量在所有線程中共享。
- default Specifies the behavior of unscoped variables in a parallel region.
- firstprivate 對於線程局部存儲的變量，其初值是進入並行區之前的值。
- if 判斷條件，可用來決定是否要並行化。
- lastprivate 在一個循環並行執行結束後，指定變量的值為循環體在順序最後一次執行時取得的值，或者#pragma sections在中，按文本順序最後一個section中執行取得的值。
- nowait 忽略barrier的同步等待。
- num_threads 設定線程數量的數量。默認值為當前電腦硬體支持的最大並發數。一般就是CPU的內核數目。超線程被作業系統視為獨立的CPU內核。
- ordered 使用於 for，可以在將循環並行化的時候，將程式中有標記 directive ordered 的部份依序執行。
- private 指定變量為線程局部存儲。
- reduction Specifies that one or more variables that are private to each thread are the subject of a reduction operation at the end of the parallel region.
- schedule 設定for循環的並行化方法；有 dynamic、guided、runtime、static 四種方法。
- schedule(static, chunk_size) 把chunk_size數目的循環體的執行，靜態依序指定給各線程。
- schedule(dynamic, chunk_size) 把循環體的執行按照chunk_size（預設值為1）分為若干組（即chunk），每個等待的線程獲得當前一組去執行，執行完後重新等待分配新的組。
- schedule(guided, chunk_size) 把循環體的執行分組，分配給等待執行的線程。最初的組中的循環體執行數目較大，然後逐漸按指數方式下降到chunk_size。
- schedule(runtime) 循環的並行化方式不在編譯時靜態確定，而是推遲到程序執行時動態地根據環境變量OMP_SCHEDULE 來決定要使用的方法。
- shared 指定變量為所有線程共享。

### OpenMP的庫函數

OpenMP定義了20多個庫函數：

1.void omp_set_num_threads(int _Num_threads);

在後續並行區域設置線程數，此調用只影響調用線程所遇到的同一級或內部嵌套級別的後續並行區域.說明：此函數只能在串行代碼部分調用.

2.int omp_get_num_threads(void);

返回當前線程數目.說明：如果在串行代碼中調用此函數，返回值為1.

3.int omp_get_max_threads(void);

如果在程序中此處遇到未使用 num_threads 子句指定的活動並行區域,則返回程序的最大可用線程數量.說明：可以在串行或並行區域調用，通常這個最大數量由omp_set_num_threads或OMP_NUM_THREADS環境變量決定.

4.int omp_get_thread_num(void);

返回當前線程id.id從1開始順序編號,主線程id是0.

5.int omp_get_num_procs(void);

返回程序可用的處理器數.

6.void omp_set_dynamic(int _Dynamic_threads);

啟用或禁用可用線程數的動態調整.(預設情況下啟用動態調整.)此調用只影響調用線程所遇到的同一級或內部嵌套級別的後續並行區域.如果 _Dynamic_threads 的值為非零值,啟用動態調整;否則,禁用動態調整.

7.int omp_get_dynamic(void);

確定在程序中此處是否啟用了動態線程調整.啟用了動態線程調整時返回非零值;否則,返回零值.

8.int omp_in_parallel(void);

確定線程是否在並行區域的動態範圍內執行.如果在活動並行區域的動態範圍內調用,則返回非零值;否則,返回零值.活動並行區域是指 IF 子句求值為 TRUE 的並行區域.

9.void omp_set_nested(int _Nested);

啟用或禁用嵌套並行操作.此調用只影響調用線程所遇到的同一級或內部嵌套級別的後續並行區域._Nested 的值為非零值時啟用嵌套並行操作;否則,禁用嵌套並行操作.預設情況下,禁用嵌套並行操作.

10.int omp_get_nested(void);

確定在程序中此處是否啟用了嵌套並行操作.啟用嵌套並行操作時返回非零值;否則,返回零值.

互斥鎖操作嵌套鎖操作功能

11.void omp_init_lock(omp_lock_t * _Lock);

- void omp_init_nest_lock(omp_nest_lock_t * _Lock);

初始化一個（嵌套）互斥鎖.

13.void omp_destroy_lock(omp_lock_t * _Lock);

14.void omp_destroy_nest_lock(omp_nest_lock_t * _Lock);

結束一個（嵌套）互斥鎖的使用並釋放記憶體.

15.void omp_set_lock(omp_lock_t * _Lock);

16.void omp_set_nest_lock(omp_nest_lock_t * _Lock);

獲得一個（嵌套）互斥鎖.

17.void omp_unset_lock(omp_lock_t * _Lock);

18.void omp_unset_nest_lock(omp_nest_lock_t * _Lock);

釋放一個（嵌套）互斥鎖.

19.int omp_test_lock(omp_lock_t * _Lock);

20.int omp_test_nest_lock(omp_nest_lock_t * _Lock);

試圖獲得一個（嵌套）互斥鎖,並在成功時放回真（true）,失敗是返回假（false）.

21.double omp_get_wtime(void);

獲取wall clock time,返回一個double的數,表示從過去的某一時刻經歷的時間,一般用於成對出現,進行時間比較.

此函數得到的時間是相對於線程的,也就是每一個線程都有自己的時間.

22.double omp_get_wtick(void);

得到clock ticks的秒數.

### 例子

在 omp parallel 段內的程序代碼由多線程來執行：

```c
 int main(int argc, char* argv[])
 {
  #pragma omp parallel  
   printf("Hello, world.\n");

   return 1;
 }
```

#### 執行結果

```
% gcc omp.c （由單線程來執行）
% ./a.out
Hello, world.

% gcc -fopenmp omp.c （由多線程來執行）
% ./a.out
Hello, world.
Hello, world.
Hello, world.
Hello, world.
```

## 環境變量

OpenMP可以使用環境變量 OMP_NUM_THREADS以控制執行線程的數量。

### 例子

```
% gcc -fopenmp omp.c 

% setenv OMP_NUM_THREADS 2（由2線程來執行）
setenv是CSH的指令

在bash shell 環境中 要用export 
% export OMP_NUM_THREADS=2 （由2線程來執行）

% ./a.out
Hello, world.
Hello, world.
```

## 優點和缺點

優點

- 可移植的多線程代碼（在C/C++和其他語言中，人們通常為了獲得多線程而調用特定於平台的原語）
- 簡單，沒必要象MPI中那樣處理消息傳遞
- 數據分布和分解由指令自動完成
- 增量並行，一次可以只在代碼的一部分執行，對代碼不需要顯著的改變
- 統一的順序執行和並行執行的代碼，在順序執行編譯器上，OpenMP的執行按照注釋進行對待。
- 在一般情況下,使用OpenMP並行時原始的（串行）代碼語句不需要進行修改,這減少不經意間引入錯誤的機會。
- 同時支持粗粒度和細粒度的並行
- 可以在GPU上使用

缺點

- 存在引入難以調試的同步錯誤和競爭條件的風險
- 目前，只能在共享內存的多處理器平台高效運行
- 需要一個支持OpenMP的編譯器
- 可擴展性是受到記憶體架構的限制
- 不支持比較和交換
- 缺乏可靠的錯誤處理
- 缺乏對線程與處理器映射的細粒度控制
- 很容易出現一些不能共享的代碼
- 多線程的可執行文件的啟動需要更多的時間，可能比單線程的運行的慢，因此，使用多線程一定要有其他有優勢的地方
- 很多情況下使用多線程不僅沒有好處，還會帶來一些額外消耗

## 爭議

作為高層抽象，OpenMP並不適合需要複雜的線程間同步和互斥的場合。OpenMP的另一個缺點是不能在非共享內存系統（如電腦集群）上使用。在這樣的系統上，MPI使用較多。

## 編譯器支持

主流C/C++編譯器，如gcc與visual C++，都內在支持OpenMP。一般都必須在程序中`#include <omp.h>`

gcc編譯時需使用編譯選項-fopenmp。但是，如果編譯為目標文件與連結生成可執行文件是分開為兩步操作，那麼連結時需要給出附加庫gomp，否則會在連結時報錯「undefined reference to `omp_get_thread_num'"。

Visual C++需要在IDE的編譯選項->語言->支持OpenMP。這實際上使用了編譯選項/openmp

## 基本資料

- 作者：OpenMP Architecture Review Board
- 開發者：OpenMP Architecture Review Board
- 軟體版本週期：5.0
- 軟體版本週期：2018年11月8日
- 程式語言：C, C++, Fortran
- 作業系統：跨平台
- 平台：跨平台
- 類型：API
- 許可協議：多種
- 網站：http://openmp.org