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 時，程式又可退化為通常的程式（一般為串行），程式碼仍然可以正常運作，只是不能利用多執行緒來加速程式執行。

## 介紹

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); 
12. 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_ 段內的程式碼由多執行緒來執行：

#### 執行結果

```
% 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 官方網站（頁面存檔備份，存於網際網路檔案館）