OpenMP
OpenMP
是一種支持多平臺的共享內(nèi)存是越、多處理器(多線程)的規(guī)范與API曹鸠。OpenMP
的API包括編譯器偽指令(pragma
指令)造成、運行時函數(shù)搞旭、環(huán)境變量幾個部分趁餐。OpenMP
簡化了并行代碼的編寫喷兼,許多細(xì)節(jié)都由庫本身去管理(但依賴關(guān)系、讀寫沖突后雷、死鎖等要靠開發(fā)者)季惯,同時如果系統(tǒng)不支持OpenMP
,代碼也會直接采用串行執(zhí)行方式臀突。OpenMP
適用的情況是基于共享內(nèi)存模型勉抓,如果是非共享內(nèi)存模型(例如分布式),則更適合適用OpenMPI
候学。
1. 編譯
使用了OpenMP
的程序要在編譯時加入-fopenmp
參數(shù)
g++ -fopenmp filename.cpp
2. 執(zhí)行模型
OpenMP
線程采用的是fork-join
方式藕筋,在需要產(chǎn)生線程的地方通過fork
的方式產(chǎn)生線程并使用共享內(nèi)存的方式來并行,最后自動使用join
回收線程梳码。
比較新的OpenMP
支持嵌套線程(nest)隐圾,在外層循環(huán)中執(zhí)行到parallel
指定的位置時伍掀,OpenMP
會產(chǎn)生多個線程,接著如果在內(nèi)層循環(huán)中再遇到了parallel
指定的并行暇藏,則會再產(chǎn)生多個線程蜜笤。但這個功能需要系統(tǒng)支持,不支持的系統(tǒng)會忽略內(nèi)層的并行盐碱。
-
task:相當(dāng)于一個函數(shù)部分(就像是
ray
中的task一樣把兔,是任務(wù)的一部分),可以使用task
構(gòu)造塊或者section指令
構(gòu)造塊瓮顽,其必須在聲明了parallel
指令的循環(huán)內(nèi)县好。
-
target: 用于異構(gòu)并行計算內(nèi)容,執(zhí)行到
target
構(gòu)造塊的代碼時暖混,系統(tǒng)會在加速器上映射相關(guān)的數(shù)據(jù)結(jié)構(gòu)聘惦,用加速器來運行代碼。
3. 內(nèi)存模型
在OpenMP
中儒恋,所有線程都可以訪問同一個內(nèi)存空間善绎,但是由于弱內(nèi)存一致性,每個線程在同一時刻看到在內(nèi)存空間中的同一變量可能值是不一致的诫尽。
- 塊內(nèi)變量訪問權(quán)限:
- shared(default):并行區(qū)域訪問到的是真正的變量禀酱。
- private:訪問到的僅僅是副本。
- flush操作:會刷新內(nèi)存空間內(nèi)變量的值牧嫉,但是并不保證順序剂跟,所以還是可能會出現(xiàn)不一致。
4. 環(huán)境變量
通過修改環(huán)境變量的值酣藻,可以對OpenMP
進行定制曹洽。
環(huán)境變量名 | 描述 |
---|---|
OMP_SCHEDULE | 運行時的負(fù)載均衡類型與循環(huán)次數(shù) |
OMP_NUM_THREADS | 并行區(qū)域的默認(rèn)線程數(shù) |
OMP_DYNAMIC | 是否可以動態(tài)調(diào)整并行區(qū)域的線程數(shù)(ture or false) |
OMP_PROC_BIND | 是否允許遷移線程(切換處理器),如果為true辽剧,則不會遷移 |
OMP_NESTED | 是否支持嵌套并行區(qū)域 |
OMP_STACKSIZE | 指定每個線程的棧的大小 |
OMP_THREAD_LIMIT | 指定了線程最多被創(chuàng)建的數(shù)量 |
5. 鎖函數(shù)
在OpenMP
中送淆,鎖的數(shù)據(jù)類型為omp_lock_t
,其基本功能就是保證線程同步怕轿。以下為常見的幾個函數(shù):
函數(shù)名 | 描述 |
---|---|
void omp_init_lock(omp_lock_t *); | 初始化線程鎖 |
void omp_set_lock(omp_lock_t *); | 獲得線程鎖 |
void omp_nuset_lock(omp_lock_t *); | 釋放線程鎖 |
void omp_destroy_lock(omp_lock_t *); | 銷毀線程鎖 |
注:初始化和銷毀鎖的操作必須由主線程在并行區(qū)域外執(zhí)行偷崩。
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
int main(int argc, char *argv[])
{
int x = 3, y = 4;
// 初始化鎖
omp_lock_t lock;
omp_init_lock(&lock);
// 偽指令設(shè)置并行,使用num_threads()設(shè)置線程數(shù)撞羽,
// 并且將x阐斜、y設(shè)置成shared狀態(tài)
// 花括號必須在下一行,而不能在#pragma后面
#pragma omp parallel num_threads(3) shared(x, y)
{
omp_set_lock(&lock); // 線程獲得鎖诀紊,保證同步
x += omp_get_thread_num(); // 當(dāng)前的線程id
y += omp_get_thread_num();
omp_unset_lock(&lock); // 釋放鎖
}
printf("x = %d, y = %d\n", x, y);
omp_destroy_lock(&lock);
return 0;
}
6. 構(gòu)造
-
parallel
#pragma omp parallel num_threads(3) shared(x, y)
parallel構(gòu)造塊里的代碼是將同一份代碼復(fù)制到多個線程共同運行谒出,也就是說同一個任務(wù)被多個線程執(zhí)行了。
parallel構(gòu)造塊有如下限制
花括號必須在下一行,而不能在#pragma后面
不能使用
nowait()
```c++
#include<iostream>
#include<omp.h>
using namespace std;
void test_omp_parallel() {
#pragma omp parallel num_threads(3)
for (int i = 0; i < 3; i++)
{
cout << "Hello, I am " << omp_get_thread_num() << ", iter " << i << endl; // 當(dāng)前的線程id
}
}
int main(int argc, char* argv[])
{
test_omp_parallel();
return 0;
}
```
-
for
#pragma omp parallel for num_threads(3) shared(x, y)
parallel for構(gòu)造塊里的代碼是將任務(wù)分割分配到多個進程中笤喳,實現(xiàn)并行完成一個任務(wù)考赛。
需要注意的是,OpenMP支持的for構(gòu)造形式有以下限制
可推測循環(huán)次數(shù)莉测、索引是整型、自增步長不變
循環(huán)是單出口單入口唧喉,不允許使用
break
捣卤、goto
、return
等語句跳出循環(huán)八孝。不能在循環(huán)內(nèi)部拋出異常董朝,但可以使用
exit()
退出整個程序,其余的線程也會同步退出干跛。
```c++
#include<iostream>
#include<omp.h>
using namespace std;
void test_omp_for() {
#pragma omp parallel for num_threads(3)
for (int i = 0; i < 3; i++)
{
cout << "Hello, I am " << omp_get_thread_num() << ", iter " << i << endl; // 當(dāng)前的線程id
}
}
int main(int argc, char* argv[])
{
test_omp_for();
return 0;
}
```
-
simd
#pragma omp simd 子句
SIMD可以使得代碼向量化子姜,也有點像SSE、AVX等向量化指令集楼入,需要
OpenMP 4.0
以上的版本才支持該偽指令哥捕。可以單獨使用嘉熊,也可以和parallel
遥赚、parallel for
聯(lián)合使用。
simd有以下子句:
* **safelen(x)**:其后緊隨的循環(huán)中阐肤,每x次循環(huán)都是互不相關(guān)的(也就是每個線程內(nèi)的數(shù)據(jù)是不相關(guān)的)凫佛。
* **aligned(list:n)**:對數(shù)組list對齊尺寸為n個字節(jié)。
* **reduction(+:ret)**:每個循環(huán)的數(shù)組都是部分?jǐn)?shù)據(jù)孕惜,最后將其整合成完整的數(shù)據(jù)愧薛。
```c++
#include<iostream>
#include<omp.h>
using namespace std;
void test_omp_simd() {
int a[4] = { 1, 2, 3, 4 };
int ret=0;
#pragma omp parallel for simd reduction(+:ret)
{
for (int i = 0; i < 4; i++) {
ret += a[i] * a[i];
}
}
cout << "ret: " << ret << endl;
}
int main(int argc, char* argv[])
{
test_omp_simd();
return 0;
}
```
-
task
#pragma omp task 子句
task主要就是為了方便分解任務(wù),但需要注意的是衫画,
task
構(gòu)造必須在parallel
構(gòu)造塊之中運行毫炉,且必須在parallel構(gòu)造塊中使用single
子句。如果需要父線程等待所有子線程完成才繼續(xù)執(zhí)行削罩,則需要使用taskwait
子句碘箍。
遞歸計算Fibonacci數(shù)列例子
```c++
#include<iostream>
#include<omp.h>
using namespace std;
int facobi(int num) {
// 遞歸退出條件
if ((0 == num) || (1 == num)) {
return 1;
}
int f1, f2;
#pragma omp task shared(f1)
f1 = facobi(num - 1);
#pragma omp task shared(f2)
f2 = facobi(num - 2);
#pragma omp taskwait
return f1 + f2; // 使用taskwait語句,等待f1和f2計算結(jié)束再返回
}
void test_omp_task() {
int r;
#pragma omp parallel shared(r)
{
#pragma omp single
r = facobi(5); // 必須使用single子句鲸郊,指定該任務(wù)只有1個線程運行
}
cout << "r: " << r << endl;
}
int main(int argc, char* argv[])
{
test_omp_task();
return 0;
}
```
-
sections/section
將代碼分成幾個段落丰榴,每個段落使用一個線程執(zhí)行,互不相干秆撮。
#pragma omp parallel section
-
single
指定某些代碼只使用一個線程運行四濒,是僅對某些代碼進行特殊處理時才使用的。
#pragma omp single
-
barrier
柵欄同步是指要執(zhí)行當(dāng)前線程,必須保證這一部分之前的所有線程都執(zhí)行完畢(比如每個并行任務(wù)中有幾塊分開的并行區(qū)域盗蟆,每一個區(qū)域執(zhí)行前戈二,必須保證上一個區(qū)域的所有線程都已執(zhí)行完)。而
barrier
則是顯式調(diào)用柵欄同步喳资。#include<iostream> #include<omp.h> using namespace std; void test_omp_barrier() { int a[6]; #pragma omp parallel num_threads(3) shared(a) { int id = omp_get_thread_num(); // 當(dāng)前的線程id a[id] = id; a[3 + id] = 3 + id; #pragma omp barrier int swap = a[id]; a[id] = a[5 - id]; a[5 - id] = swap; } for (int i = 0; i < 6; i++) cout << "a[" << i << "]: " << a[i] << endl; } int main(int argc, char* argv[]) { test_omp_barrier(); return 0; }
-
proc_bind
處理器映射策略觉吭,有以下幾種策略
spread:均勻分布在各個核心
close:盡量分布在相鄰的處理器
master:主線程和子線程都在同一處理器
-
nowait
顯示地取消了柵欄同步。
7. 子句
-
collapse
為了負(fù)載均衡(例如緊隨其后的兩個循環(huán)仆邓,每個循環(huán)都較邢侍病),合并之后任務(wù)較小的循環(huán)节值。
// 意思是指定接下來的兩層循環(huán)直接線程化 #pragma omp parallel for collapse(2) for(int i = 0; i < 3; i++){ for(int j = 0; j < num; j++){ // ..... } }
-
private
將某個線程私有化徙硅,每個線程擁有變量的副本,且不允許其他線程染指這些變量搞疗。
-
shared
聲明某些變量是共享的嗓蘑,但必須保證讀寫的正確性。
-
reduction
在并行區(qū)域結(jié)束時匿乃,對指定的某個變量進行某個計算操作(例如+桩皿,支持的運算符只有少數(shù))。
-
schedule
schedule(type[, size])
其中幢炸,size是可選參數(shù)业簿,表示每次分發(fā)任務(wù)的循環(huán)迭代次數(shù)。而type為策略阳懂,主要有以下幾種
- static:靜態(tài)負(fù)載均衡策略梅尤,設(shè)任務(wù)長度為K, 而線程數(shù)量為N岩调,則每個線程分得的任務(wù)為 K/N巷燥。因為 K/N不一定是整數(shù),所以存在一定的負(fù)載均衡問題号枕。
- dynamic:動態(tài)負(fù)載均衡策略缰揪,根據(jù)設(shè)定的size(或默認(rèn))將任務(wù)分割,加入工作隊列葱淳,哪個線程空閑就去領(lǐng)取新的任務(wù)钝腺,直到所有任務(wù)被執(zhí)行完畢。
-
runtime:由環(huán)境變量
OMP_SCHEDULE
的值來確定負(fù)載均衡策略赞厕。
-
if
根據(jù)所給條件艳狐,來決定該區(qū)域什么時候并行,什么時候不采取并行皿桑。