前情提要:並行(parallel):需要多個運算核心同時完成
其中有多處理器和單處理器多核兩種實現方式,其中差異如下:
筆者當初剛進入的時候,走了許多彎路,這裡給出Dev-Cpp版本的OpenMP的環境設定
點選Tools,再點選裡面的Complier Options,在裡面的General下的Add the following commands when calling the compiler:加入這段控制語句-fopenmp就可以在Dev-Cpp中編寫omp程式
預處理指令
#pragma once
該段程式碼實現的功能是標頭檔案只被編譯一次
#ifdef HEADER_H
#define HEADER_H
...
#endif
上述程式碼實現的功能也是標頭檔案只被編譯一次
#pragma once | #ifndef |
---|---|
需要編譯器支援 | 不需要特定編譯器 |
針對物理檔案 | 不針對物理檔案 |
需要使用者保證標頭檔案沒有多份拷貝 | 需要使用者保證不同檔案的宏名不重複 |
#pragma GCC poison printf
禁止使用printf函數,該段語句本質上就是申明printf被汙染了,緊跟在該條語句後面的程式碼段不能使用printf。
注意:對於形如#pragma的語句,其作用域一般只包含於該語句的下一個程式碼段,也就是一對大括號之內。
對於#pragma還有許多語法,類似
#pragma startup
#pragma exit
#pragma warning
這些語句對於gcc編譯器來說,不能進行正常的編譯,直接會被忽視,並不會執行,因此這裡不表
總之,對於編譯器來說如果不支援#pragma指令則會直接將其忽視,並不會執行
檢視OpenMP版本
使用OpenMP編寫第一個程式
#include<stdio.h>
#include<omp.h>
int main() {
#pragma omp parallel
{
printf("Hello World\n");
}
return 0;
}
我們一般通過 #pragma omp parallel 指明並行部分
執行緒的編號獲取是通過下列語句
omp_get_thread_num();
總共的執行緒總數獲取是通過下列語句
omp_get_max_threads();
同一執行緒的多個語句一般情況下並不是連續執行,其中可能有其他執行緒加入,導致該執行緒阻塞。
OpenMP執行機制
fork : 由主執行緒(master thread)建立一組從執行緒(slave threads)
join : 同步終止所有執行緒並將控制權轉移回至主執行緒
fork是各奔東西,join是百川入海
#pragma omp parallel [clause[clause]...]{stuctured block}
num_threads(int)
omp_set_num_threads(int)
修改全域性預設執行緒數,注意這需要在外面修改,線上程中修改筆者嘗試後沒有作用這裡做個區分
並行控制指令 | 功能 |
---|---|
omp_get_num_threads |
返回當前並行區域中的活動執行緒個數,如果在並行區域外部呼叫,返回1 |
omp_get_thread_num |
返回當前的執行緒號,注意不要和之前的omp_get_num_threads混餚 |
omp_set_num_threads |
設定進入並行區域時,將要建立的執行緒個數 |
omp_get_max_threads |
用於獲得最大的執行緒數量,這個最大數量是指在不使用num_threads的情況下,OpenMP可以建立的最大執行緒數量。需要注意這個值是確定的,與它是否在並行區域呼叫沒有關係 |
#pragma omp parallel
{
int n;
for(n = 0; n < 4; n++) {
int thread = omp_get_thread_num();
printf("thread %d\n", thread);
}
}
該段語句是每個執行緒都執行了一次該回圈體中的內容,也就是最後的結果會發現每個執行緒都執行了四次printf("thread %d\n", thread)
語句。
如果想要將回圈中的迭代分配到多個執行緒並行,有兩種方式,
方式1
#pragma omp parallel
{
int n;
#pragma omp for
for(n = 0; n < 4; n++) {
int thread = omp_get_thread_num();
printf("thread %d]n", thread);
}
}
在並行區域內加入#pragma omp for
注意:在並行區域內,for迴圈外還可以加入其他並行程式碼
方式2
int n;
#pragma omp parallel for
for(n = 0; n < 4; n++) {
int thread = omp_get_thread_num();
printf("thread %d\n", thread);
}
合併為#pragma omp parallel for
,這樣子寫法更加簡潔
OpenMP中的每個執行緒同樣可以被並行化為一組執行緒
omp_set_nested(1)
開啟當迴圈分配並行的時候,有下列限制:
迴圈迭代相關(loop-carried dependence)
非迴圈迭代相關(loop-independent dependence)
如果資料與迴圈無關,那麼普通的迴圈並行是可以的,但是如果與迴圈有關,那麼並行就會出現問題
#include<stdio.h>
#include<omp.h>
int main() {
int a[4] = {1, 2, 3, 4};
#pragma omp parallel for
for(int i = 1; i < 4; i++) {
a[i] = a[i-1] + 2;
}//這裡執行出來的結果不一定是1,3,5,7,這就是迴圈迭代相關
return 0;
}
比如count++並不是只有一個子操作,而是返回count值後再進行++操作。
對於count++其所包含的操作如下
LOAD Reg, count
ADD #1
STORE Reg, count
包含三個子操作,如果一個執行緒負責count++,另一個執行緒負責count--,那麼就有可能出現下面的情況(競爭條件)
thread 1 thread 2
Reg count Reg
LOAD 10 10
ADD 11 10
11 10 10 LOAD
11 10 9 SUB
11 9 9 STORE
STORE 11 11
同樣如果上述的模板中thread 1執行的是count--,而thread 2執行的是count++,那麼最終count儲存的是9,這樣子就會因為執行順序不同而造成不同的結果
#pragma omp critical
指的是一個存取共用資源(例如:共用裝置或是共用記憶體)的程式片段,而這些共用資源又無法同時被多個執行緒存取的特性
如果具有競爭條件,那麼就可以使用臨界區來保證結果的正確,此時被#pragma omp critical
修飾的程式碼段對於每個執行緒來說此時裡面的資源是它獨佔的,其他執行緒無權存取修改,因此可以消除競爭條件帶來的不確定性
#pragma omp parrallel for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
histogram[value]++;
}
int total = 0;
for(int i = 0; i < 20; i++) {
total +== histogram[i];
cout << histogram[i] << endl;
}
cout << "total:" << total << endl;
本段程式碼的輸出未必是1000,原因可以參考前面的count++,++並非原子操作,因此多執行緒使用的時候會形成競爭條件,導致最後的儲存除了問題
解決方案(新增臨界區):
#pragma omp parallel for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp critical
{
histogram[value]++;
}
}
int total = 0;
for(int i = 0; i < 20; i++) {
total += histogram[i];
cout << histogram[i] << endl;
}
cout << "total:" << total << endl;
#pragma omp atomic
程式碼演示如下:
#pragma omp parallel for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
關於原子操作和臨界區的一些總結
指令 | 功能 |
---|---|
臨界區 | 存取臨界資源的程式碼,一個執行緒進入了臨界區,如果第二個執行緒不會進入臨界區那麼第二個執行緒也可以執行 |
原子操作 | 原子操作獨佔處理器,其他執行緒必須等原子操作完了才可以執行 |
原子操作是山大王,臨界區是此路是我開,此樹是我栽
總的來說就是,原子操作時不允許其他執行緒搶佔資,也就是不能中斷,但是臨界區沒有這個硬性規定,只是相關的程式碼區段此時只能有一個執行緒執行。
#pragma omp barrier
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<omp.h>
int main() {
srand(time(NULL));
int total = 0;
int histogram[20];
memset(histogram, 0, sizeof(histogram));
#pragma omp parallel num_threads(20)
{
for(int i = 0; i < 50; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
int thread = omp_get_thread_num();
#pragma omp atomic
total += histogram[thread];
}
return 0;
}
這裡如果沒有加入柵障指令,那麼最後統計的total一般不是1000,造成資料丟失,因為不能保證執行到total += histogram[thread]
對應的數位已經全部統計完畢,其他執行緒可能仍然在統計
修改方案如下:
int total = 0;
#pragma omp parallel num_threads(20)
{
for(int i = 0; i < 50; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
#pragma omp barrier//保證執行到下一個語句之前,所有的執行緒都已經統計完畢了亂數的次數
int thread = omp_get_thread_num();
#pragma omp atomic
total += histogram[thread];
}
這裡給出#pragma omp for
的一些相似之處
int total = 0;
#pragma omp parallel num_threads(20)
{
#pragma omp for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
int thread = omp_get_thread_num();
#pragma omp atomic
total += histogram[thread];
}
這裡的for分配出來的結果仍然是1000,也就是說#pragma omp for
也是等到最後的執行緒執行完迴圈才進行下一步任務,前面先完成的執行緒會進入等待狀態。注意如果修改成#pragma omp for nowait
,那麼結果就會不確定,因為此時先完成的執行緒不會再等待了(產生了一個隱式柵障,但是nowait可以去除)。
#pragma omp single{}
#pragma omp single nowait
去除#pragma omp master{}
#pragma omp parallel IF(omp_get_thread_num() == 0) nowait
,這邊的話,筆者暫時沒有找到相關的資料,不過可以理解為只有主執行緒執行該程式碼塊,而其他執行緒直接跳過,可以執行後面的任務。#pragma omp master
使用如下
int total = 0;
#pragma omp parrallel
{
#pragma omp for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
#pragma omp master
{
for(int i = 0; i < 20; i++) {
total += histogram[i];
}
}
}
注意single會產生柵障,但是master不會
single獨樂樂,master眾樂樂
指明如何將執行緒區域性結果彙總
#pragma omp for reduction(+:total)
+
-
*
&
|
&&
||
^
#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
#include<time.h>
int main() {
int total = 0;
int histogram[20];
memset(histogram, 0, sizeof(histogram));
#pragma omp parallel
{
#pragma omp for
for(int i = 0; i < 1000; i++) {
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
}
#pragma omp for reduction(+:total)
for(int i = 0; i < 20; i++) {
total += histogram[i];
}
}
printf("%d\n", total);
return 0;
}
reduction指令就是指明如何將執行緒區域性結果彙總,原先沒有reduction限制的時候,total是每個執行緒共用的,也就是所有的執行緒都可以存取修改total的值。但是加入reduction限制後,每個執行緒的total都是獨屬於該執行緒的全新total,執行緒1和執行緒2的total沒有關係,也就是執行緒1的total的值改變了,不會影響到執行緒2的total的值。這樣每個執行緒都有自己獨特的total,等到所有執行緒結束完成之後,再根據reduction後面所給的操作符,將所有執行緒的total經過該運算儲存到原來的total中。
Shared | Private |
---|---|
變數為所有執行緒所共用 | 變數為執行緒私有,其他執行緒無法存取 |
並行區域外定義的變數預設為shared | 並行區域內定義的變數預設為private |
迴圈計數器預設為private |
int histogram[20];//histogram是shared
memset(histogram, 0, sizeof(histogram));
int total = 0;//total是shared
int i,j;
#pragma omp parallel for
for(i = 0; i < 1000; i++) {//迴圈計數器i是private,雖然定義在並行區域外部
int value = rand()%20;
#pragma omp atomic
histogram[value]++;
for(j = 0; j < 1000; j++) {//迴圈計數器j也是private
...
}
}
shared(var)
default(none/shared/private)
int a, b, c;
...//init a, b, c
#pragma omp parallel default(shared) shared(a)
{
b += a;
}
這裡會報錯,因為b並沒有指定是private還是shared,注意只要default中選擇的是none,那麼並行體中出現的變數就都要說明是shared還是private型別。
int sum = 0;
#pragma omp parallel for
for(int i = 0; i < 20; i++) {
sum += i;
}
這裡連續和的答案不確定,也是因為sum是shared,shared本質上就是說所有執行緒的sum指向的都是同一塊記憶體空間
private(var)
int i = 10;
#pragma omp parallel for
for(int j = 0; j < 4; j++) {
printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
}
printf("i = %d\n", i);
這裡執行緒中輸出的i的值都是未定義的,因為此時i是private,也就是說,對於每個執行緒來說,他們的i都是相互獨立的,擁有自己的空間。所以對於每個執行緒,他們的i都需要重新初始化。firstprivate(var)
int i = 10;
#pragma omp parallel for firstprivate(i)
for(int j = 0; j < 4; j++) {
printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
}
printf("i = %d\n", i);
這裡輸出的結果i都是10,也就是說firstprivate(var)的作用就是仍然申明i是private,不過此時不需要我們顯式初始化,而是隱式的初始化,初始化的值是主執行緒中的i的值。int i = 10;
#pragma omp parallel for lastprivate(i)
for(int j = 0; j < 4; j++) {
printf("Thread %d: i = %d\n", omp_get_thread_num(), i);
}
printf("i = %d\n", i);
這邊會發現程式執行的結果,i的結果和最後一個執行緒,在這邊是執行緒3中的i一致。所以我們可以得出lastprivate(var)
的功能就是將最後一個執行緒中的變數的值在join後賦值給原先主執行緒的。資料並行
任務並行
#pragma omp section
方式1:
#paragma omp parallel
#pragma omp sections
{
#pragma omp section
task_A();
#pragma omp section
task_B();
#pragma omp section
task_C();
}
方式2:
#pragma omp parallel sections
{
#pragma omp section
task_A();
#pragma omp section
task_B();
#pragma omp section
task_C();
}
#pragma omp sections
的語法如上,此時裡面的每個#pragma omp section
都只會由一個執行緒來執行,如果沒有加nowait
,那麼直到最後一個任務完成,所有的執行緒才能繼續往下,否則將在這裡等待,換句話說sections
指令有隱式柵障。自測程式如下,加上nowait和不加輸出結果是不同的。
#include<stdio.h>
#include<omp.h>
int main() {
#pragma omp parallel
{
#pragma omp sections //nowait//這裡證明了sections自帶隱式柵障
{
#pragma omp section
{
int thread = omp_get_thread_num();
printf("Thread %d : tastk 1\n", thread);
}
#pragma omp section
{
int thread = omp_get_thread_num();
printf("Thread %d : task 2\n", thread);
}
#pragma omp section
{
int thread = omp_get_thread_num();
printf("Thread %d : task 3\n", thread);
}
}
int thread = omp_get_thread_num();
printf("Thread %d : task 4\n", thread);
}
return 0;
}
某些執行緒將執行多個迭代
#pragma omp parallel for schedule(type,[chunk size])
type
包括static
,dynamic
,guided
,auto
,runtime
static
排程方式 | 作用&缺陷 |
---|---|
static |
執行緒的負載可能不均勻 |
dynamic |
在執行中動態分配任務;迭代任務依然根據chunk size劃分成塊;執行緒完成一個chunk後向系統請求下一個chunk |
guided |
與dynamic類似;但分配的chunk大小在執行中遞減,最小不能小於chunk size引數 |
auto &runtime |
編譯器決定策略與環境變數指定策略 |
總的來說auto
和runtime
其實最後都是在static
,dynamic
,guided
中挑選一個策略。所以理解這三種排程方式最為重要。
#pragma omp parallel for schedule(static)
#pragma omp parallel for schedule(static, 5)
#pragma omp parallel for schedule(dynamic)
#pragma omp parallel for schedule(dynamic, 5)
#pragma omp parallel for schedule(guided)
#pragma omp parallel for schedule(guided, 5)
軟硬體環境
基礎語法
老祖宗:#pragma omp construct [clause[clause]...]{structured block}
分類 | 語法 |
---|---|
指明並行區域 | #pragma omp parallel |
迴圈 | #pragma omp (parallel) for |
巢狀 | omp_set_nested(1) |
常用函數 | omp_get_thread_num() ,num_threads(int) |
同步 | #pragma omp critical ,#pragma omp atomic ,#pragma omp barrier ,nowait |
變數作用域 | default(none/shared/private) ,shared() ,private() ,firstprivate() ,lastprivate() |
排程 | schedule(static/dynamic/guided,[chunk_size]) |
文末資源分享:
禁止轉載,如需轉載請在評論區與筆者商量或者私信。