OpenMP: 多執行緒檔案操作
OpenMP:多執行緒檔案操作
本部落格轉載自: https://blog.csdn.net/augusdi/article/details/8806306.
簡介
OpenMP是由OpenMP Architecture Review Board牽頭提出的,並已被廣泛接受,用於共享記憶體並行系統的多處理器程式設計的一套指導性編譯處理方案(Compiler Directive) 。OpenMP支援的程式語言包括C、C++和Fortran;而支援OpenMp的編譯器包括Sun Compiler,GNU Compiler和Intel Compiler等。OpenMp提供了對並行演算法的高層的抽象描述,程式設計師通過在原始碼中加入專用的pragma來指明自己的意圖,由此編譯器可以自動將程式進行並行化,並在必要之處加入同步互斥以及通訊。當選擇忽略這些pragma,或者編譯器不支援OpenMp時,程式又可退化為通常的程式(一般為序列),程式碼仍然可以正常運作,只是不能利用多執行緒來加速程式執行。
具體實現
- OpenMP編譯必須包含標頭檔案<omp.h>.
- 通過預處理指示符 #pragma omp 來表示使用OpenMP. 例如通過 #pragma om parallel for 來指定下方的for迴圈採用多執行緒執行,此時編譯器會根據CPU的個數來建立執行緒數。對於雙核系統,編譯器會預設建立兩個執行緒執行並行區域的程式碼。
示例程式碼:
#include <iostream>
#include <stdio.h>
#include <omp.h> // OpenMP編譯需要包含的標頭檔案
int main()
{
#pragma omp parallel for
for (int i = 0; i < 100; ++i)
{
std::cout << i << std::endl;
}
return 0;
}
1. OpenMP常用函式
函式原型 | 功能 |
---|---|
int omp_get_num_procs(void) | 返回當前可用的處理器個數 |
int omp_get_num_threads(void) | 返回當前並行區域中活動執行緒的個數,如果在並行區域外部呼叫,返回1 |
int omp_get_thread_num(void) | 返回當前的執行緒號(omp_get_thread_ID更好一些) |
int omp_set_num_threads(void) | 設定進入並行區域時,將要建立的執行緒個數 |
2. 並行區域
#pragma omp parallel //大括號內為並行區域
{
//put parallel code here.
}
- 庫函式示例
#include <iostream>
#include <omp.h>
int main()
{
std::cout << "Processors Number: " << omp_get_num_procs() << std::endl;
std::cout << "Parallel area 1" << std::endl;
#pragma omp parallel
{
std::cout << "Threads number: " << omp_get_num_threads() << std::endl;
std::cout << "; this thread ID is " << omp_get_thread_num() << std::endl;
}
std::cout << "Parallel area 2" << std::endl;
#pragma omp parallel
{
std::cout << "Number of threads: " << omp_get_num_threads() << std::endl;
std::cout << "; this thread ID is " << omp_get_thread_num() << std::endl;
}
return 0;
}
3. for 迴圈並行化基本用法
3.1 資料不相關性
利用 OpenMP 實現for迴圈的並行化,需滿足資料的不相關性。
在迴圈並行化時,多個執行緒同時執行迴圈,迭代的順序是不確定的。如果資料是非相關的,那麼可以採用基本的 #pragma omp parallel for 預處理指示符。
如果語句S2與語句S1相關,那麼必然存在以下兩種情況之一:
1. 語句S1在一次迭代中訪問儲存單元L,而S2在隨後的一次迭代中訪問同一儲存單元,稱之為迴圈迭代相關(loop carried dependence);
2. S1和S2在同一迴圈迭代中訪問同一儲存單元L,但S1的執行在S2之前,稱之為非迴圈迭代相關(loop-independent dependence)。
3.2 for迴圈並行化的幾種宣告形式
#include <iostream>
#include <omp.h>
int main()
{
//宣告形式一
#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < 10; ++i)
{
std::cout << i << std::endl;
}
}
//宣告形式二
#pragma omp parallel for
for (int i = 0; i < 10; ++i)
{
std::cout << i << std:: endl;
}
return 0;
}
上面程式碼的兩種宣告形式是一樣的,可見第二種形式更為簡潔。不過,第一種形式有一個好處:可以在並行區域內、for迴圈以外插入其他並行程式碼。
//宣告形式一
#pragma omp parallel
{
std::cout << "OK." << std::endl;
#pragma omp for
for(int i = 0; i < 10; ++i)
{
std::cout << i << std::endl;
}
}
//宣告形式二
#pragma omp parallel for
//std::cout << "OK." << std::endl; // error!
for(int i = 0; i < 10; ++i)
{
std::cout << i << std::endl;
}
3.3 for 迴圈並行化的約束條件
儘管OpenMP可以很方便地對for迴圈進行並行化,但並不是所有的for迴圈都可以並行化。下面幾種情形的for迴圈便不可以:
1. for迴圈的迴圈變數必須是有符號型。例如,for(unsigned int i = 0; i < 10; ++i){…}編譯不通過。
2. for迴圈的比較操作符必須是== <, <=, >, >= 。例如,for(int i = 0; i != 10; i++)編譯不通過。
3. for迴圈的增量必須是整數的加減,而且必須是一個迴圈不變數。例如,for(int i = 0; i < 10; i = i+1)編譯不通過,感覺只能++i, i++, --i, i–。
4. for迴圈的比較操作符如果是<, <=,那麼迴圈變數只能增加。例如,for(int i = 0; i != 10; --i)編譯不通過。
5. 迴圈必須是單入口,單出口==。迴圈內部不允許能夠達到迴圈以外的跳出語句,exit除外。異常的處理也不必須在迴圈體內部處理。例如,如迴圈體內的break或者goto語句,會導致編譯不通過。
- 基本 for 迴圈並行化示例
#include <iostream>
#include <omp.h>
int main()
{
int a[10] = {1};
int b[10] = {2};
int c[10] = {3};
#pragma omp parallel
{
#pragma omp for
for(int i = 0; i < 10; ++i)
{
//c[i]只與a[i]和b[i]相關
c[i] = a[i] + b[i];
}
}
return 0;
}
- 巢狀 for 迴圈
#include <iostream>
#include <omp.h>
int main()
{
#pragma omp parallel
{
#pragma omp for
for(int i = 0; i < 10; ++i)
{
for(int j = 0; j < 10; ++j)
{
c[i][j] = a[i][j] + b[i][j];
}
}
}
return 0;
}
編譯器會讓第一個CPU完成
for(int i = 0; i < 5; ++i)
{
for(int j = 0; j < 5; ++j)
{
c[i][j] = a[i][j] + b[i][j];
}
}
讓第二個CPU完成
for(int i = 5; i < 10; ++i)
{
for(int j = 5; j < 10; ++j)
{
c[i][j] = a[i][j] + b[i][j];
}
}
4. 資料的共享和私有化
4.1 引言
在並行區域內,若多個執行緒共同訪問同一個儲存單元,並且至少會有一個執行緒更新資料單元中的內容時,會發生資料競爭。本節的資料共享和私有化對資料競爭做一個初步探討,後續會涉及同步、互斥的內容。
4.2 並行區域內的變數共享和私有
除了以下三種情況外,並行區域中的所有變數都是共享的:
- 並行區域中定義的變數;
- 多個執行緒用來完成迴圈的迴圈變數;
- private、firstprivate、lastprivate、reduction修飾的變數;
例如:
#include <iostream>
#include <omp.h>
int main()
{
int share_a = 0; // 共享變數
int share_to_private_b = 1;
#pragma omp parallel
{
int private_c = 2;
//通過private修飾後在並行區域內變為私有變數
#pragma omp for private(share_to_private_b)
for(int i = 0; i < 10; ++i)
{//該迴圈變數是私有的,若為兩個執行緒,則一個執行0<=i<5,另一個執行5<=i<10
std::cout << i << std::endl;
}
}
return 0;
}
4.3 共享與私有變數宣告的方法
- private(val1, val2, …) : 並行區域中變數val是私有的,即每個執行緒擁有該變數的一個copy
- firstprivate(val1, val2, …) : 與private不同,每個執行緒在開始的時候都會對該變數進行一次初始化
- lastprivate(val1, val2, …) : 與private不同,併發執行的最後一次迴圈的私有變數將會copy到val
- shared(val1, val2, …) : 宣告val是共享的
4.4 private示例
如果使用private,無論該變數在並行區域外是否初始化,在進入並行區域後,該變數均不會初始化。
在VS2010下,會因為private所導致的私有變數未初始化而出現錯誤。例如:
#include <iostream>
#include <omp.h>
int main()
{
//通過private修飾該變數之後在並行區域內變為私有變數,進入並行
//區域後每個執行緒擁有該變數的拷貝,並且都不會初始化
int shared_to_private = 1;
#pragma omp parallel for private(shared_to_private)
for(int i = 0; i < 10; ++i)
{
std::cout << shared_to_private << std::endl;
}
return 0;
}
F5除錯由於變數shared_to_rivate未初始化而崩掉。
4.5 firstprivate 示例
#include <iostream>
#include <omp.h>
int main()
{
//通過firstprivate修飾該變數之後在並行區域內變為私有變數,
//進入並行區域後每個執行緒擁有該變數的拷貝,並且會初始化
int share_to_first_private = 1;
#pragma omp parallel for firstprivate(share_to_first_private)
for(int i = 0; i < 10; ++i)
{
std::cout << ++share_to_first_private << std::endl;
}
return 0;
}
執行程式,可以看到每個執行緒對應的私有變數share_to_first_private都初始化為1,並且每次迴圈各自增加1。
4.6 lastprivate 示例
#include <iostream>
#include <omp.h>
int main()
{
//通過lastprivate修飾後在並行區域內變為私有變數,進入並行區域
//後變為私有變數,進入並行區域後每個執行緒擁有該變數的拷貝,並且會初始化
int share_to_last_private = 1;
std::cout << "Before: " << share_to_last_private << std::endl;
#pragma omp parallel for lastprivate(share_to_last_private)firstprivate(share_to_last_private)
for(int i = 0; i < 11; ++i)
{
std::cout << ++share_to_last_private << std::endl;
}
std::cout << "After: " << share_to_last_private << std::endl;
return 0;
}
同樣,仍然需要通過firstprivate來初始化並行區域中的變數,否則執行會出錯。
在執行前後,share_to_last_private變數的值變了,其值最後變成最後一次迴圈的值,即多個執行緒最後一次修改的share_to_last_private(是share_to_last_private的copy)值會賦給share_to_last_private。
4.7 shared 示例
#include <iostream>
#include <omp.h>
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for shared(sum)
for(int i = 0; i < 10; ++i)
{
sum += i;
std::cout << sum << std::endl;
}
std::cout << "After: " << sum << std::endl;
return 0;
}
上面的程式碼中,sum本身就是共享的,這裡的shared的宣告作為演示用。上面的程式碼因為sum是共享的,多個執行緒對sum的操作會引起資料競爭,後續在做介紹。
4.8 reduction 的用法
#include <iostream>
#include <omp.h>
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for reduction(+:sum)
for(int i = 0; i < 10; ++i)
{
sum = sum + i;
std::cout << sum << std::endl;
}
std::cout << "After: " << sum << std::endl;
return 0;
}
其中sum是共享的,採用reduction之後,每個執行緒根據reduction(+:sum)的宣告算出自己的sum,然後再將每個執行緒的sum加起來。
執行程式,發現第一個執行緒sum的值依次為0、1、3、6、10;第二個執行緒sum的值依次為5、11、18、26、35;最後10+35=45。
計算步驟如下:
第一個執行緒sum=0,第二個執行緒sum=5
第一個執行緒sum=2+12=14;第二個執行緒sum=7+14=21
第一個執行緒sum=3+21=24;第二個執行緒sum=8+24=32
第一個執行緒sum=4+32=36;第二個執行緒sum=9+36=45
儘管結果是對的,但是兩個執行緒對共享的sum的操作時不確定的,會引發資料競爭,例如計算步驟可能如下:
第一個執行緒sum=0,第二個執行緒sum=5
第一個執行緒sum=1+5=6;第二個執行緒sum=6+6=12
第一個執行緒sum=2+12=14;第二個執行緒sum=7+14=21
第一個執行緒sum=3+21=24;第二個執行緒sum=8+21=29 //在第一個執行緒沒有將sum更改為24時,第二個執行緒讀取了sum的值
第一個執行緒sum=4+29=33;第二個執行緒sum=9+33=42 //導致結果錯誤。
-
reduction 宣告可以看作:
- 保證了對sum的原則操作
- 多個執行緒的執行結果通過reduction中宣告的操作符進行計算,以加法操作符為例:
假設sum的初始化為10,reduction(+:sum)宣告的並行區域中每個執行緒的sum初始化為0(規定),並行處理結束之後,會將sum的初始化值10以及每個執行緒所計算的sum值相加。
-
reduction 宣告形式:
其具體如下:
reduction(operator: val1, val2, …) -
其中operator以及約定變數的初始值如下:
運算子 | 資料型別 | 預設初始值 |
---|---|---|
+ | 整數,浮點 | 0 |
- | 整數,浮點 | 0 |
* | 整數,浮點 | 1 |
& | 整數 | 所有位均為1 |
| | 整數 | 0 |
^ | 整數 | 0 |
&& | 整數 | 1 |
|| | 整數 | 0 |
5. 互斥鎖同步機制與事件同步機制
5.1 互斥鎖同步
互斥鎖同步的概念類似於Windows中的臨界區(Critical Section)以及Windows和Linux中的Mutex以及VxWorks中的SemTake和SemGive(初始化時訊號量為滿),即對某一塊程式碼操作進行保護,以保證同時只能有一個執行緒執行該段程式碼。
5.2 atomic 同步語法
#pragma omp atomic
x < + or * or - or * or / or & or | or << or >> >=expt
(例如,x<<=1; or x*=2;)
或
#prgma omp atomic
x++ or x-- or --x or ++x
可以看到atomic的操作僅適用於兩種情況:
1. 自加減操作;
2. x<上述列出的操作符>=expr;
示例:
#include <iostream>
#include <omp.h>
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for
for(int i = 0; i < 2000; ++i)
{
#pragma omp atomic
sum++;
}
std::cout << "After: " << sum << std::endl;
return 0;
}
輸出2000,如果將#pragma omp atomic宣告去掉,則結果不確定。
5.3 critical 同步機制
本節介紹互斥鎖機制的使用方法,類似於windows下的Critical Section。
- Critical Section 宣告方法:
#pragma omp critical [(name)] //[]表示名字可選
{
//並行程式塊,同時只能有一個執行緒能訪問該並行程式塊
}
例如,
#pragma omp critial (tst)
a = b + c;
- critical section 與 atomic 區別:臨界區critical可以對某個並行程度塊進行保護,atomic所能保護的僅為一句程式碼。
示例:
#include <iostream>
#include <omp.h>
int main()
{
int sum = 0;
std::cout << "Before: " << sum << std::endl;
#pragma omp parallel for
for(int i = 0; i < 10; ++i)
{
#pragma omp critial (a)
{
sum = sum + i;
sum = sum + i*2;
}
}
std::cout << "After: " << sum << std::endl;
return 0;
}
5.4 執行緒同步之互斥鎖函式
前文介紹了互斥鎖同步的兩種方法:atomic和critical,本章介紹OpenMP提供的互斥鎖函式。互斥鎖函式類似於Windows、Linux下的mutex。
- 互斥鎖函式:
函式宣告 | 功能 |
---|---|
void omp_init_lock(omp_lock*) | 初始化互斥器 |
void omp_destroy_lock(omp_lock*) | 銷燬互斥器 |
void omp_set_lock(omp_lock*) | 獲得互斥器 |
void omp_unset_lock(omp_lock*) | 釋放互斥器 |
void omp_test_lock(omp_lock*) | 試圖獲得互斥器,如果獲得成功則返回true,否則返回false |
示例:
#include <iostream>
#include <omp.h>
static omp_lock_t lock;
int main()
{
omp_init_lock(&lock); //初始化互斥鎖
#pragma omp parallel for
for(int i = 0; i < 5; ++i)
{
omp_set_lock(&lock); //獲得互斥器
std::cout << omp_get_thread_num() << "+" << std::endl;
std::cout << omp_get_thread_num() << "-" << std::endl;
omp_unset_lock(&lock); //釋放互斥器
}
omp_destroy_lock(&lock); //銷燬互斥器
return 0;
}
上邊的示例對for迴圈中的所有內容進行加鎖保護,同時只能有一個執行緒執行for迴圈中的內容。
執行緒1或執行緒2在執行for迴圈內部程式碼時不會被打斷。如果刪除程式碼中的獲得鎖釋放鎖的程式碼,則相當於沒有互斥鎖。
互斥鎖函式中只有omp_test_lock函式是帶有返回值的,該函式可以看作是omp_set_lock的非阻塞版本。
5.5 執行緒同步之事件同步機制
5.5.1 引言
前邊已經提到,執行緒的同步機制包括互斥鎖同步和事件同步。互斥鎖同步包括atomic、critical、mutex函式,其機制與普通多執行緒同步的機制類似。而事件同步則通過nowait、sections、single、master等預處理指示符宣告來完成。
5.5.2 隱式柵障
在開始之前,先介紹一下並行區域中的隱式柵障。
柵障(Barrier)是OpenMP用於執行緒同步的一種方法。執行緒遇到柵障時必須等待,直到並行的所有執行緒都到達同一點。
注意:
在任務分配for迴圈和任務分配section結構中隱含了柵障,在parallel, for, sections, single結構的最後,也會有一個隱式的柵障。
隱式的柵障。
隱式的柵障會使執行緒等到所有的執行緒繼續完成當前的迴圈、結構化塊或並行區,再繼續執行後續工作。可以使用nowait去掉這個隱式的柵障。
5.5.3 nowait事件同步
nowait用來取消柵障,其用法如下:
#pragma omp for nowait //不能使用#pragma omp parallel for nowait
或
#pragma omp single nowait
示例:
#include <iostream>
#include <omp.h>
int main()
{
#pragma omp parallel
{
#pragma omp for nowait
for(int i = 0; i < 1000; ++i)
{
std::