OpenMP並行程序設計——for循環並行化詳解

在C/C++中使用OpenMP優化代碼方便又簡單,代碼中須要並行處理的每每是一些比較耗時的for循環,因此重點介紹一下OpenMP中for循環的應用。我的感受只要掌握了文中講的這些就足夠了,若是想要學習OpenMP能夠到網上查查資料。ios

    工欲善其事,必先利其器。若是尚未搭建好omp開發環境的能夠看一下OpenMP並行程序設計——Eclipse開發環境的搭建編程

   首先,如何使一段代碼並行處理呢?omp中使用parallel制導指令標識代碼中的並行段,形式爲:數組

           #pragma omp parallel多線程

           {學習

             每一個線程都會執行大括號裏的代碼優化

            }spa

好比下面這段代碼:.net


#include <iostream>
#include "omp.h"
using namespace std;
int main(int argc, char **argv) {
//設置線程數,通常設置的線程數不超過CPU核心數,這裏開4個線程執行並行代碼段
omp_set_num_threads(4);
#pragma omp parallel
{
cout << "Hello" << ", I am Thread " << omp_get_thread_num() << endl;
}
}
omp_get_thread_num()是獲取當前線程id號  
以上代碼執行結果爲:線程


Hello, I am Thread 1
Hello, I am Thread 0
Hello, I am Thread 2
Hello, I am Thread 3
能夠看到,四個線程都執行了大括號裏的代碼,前後順序不肯定,這就是一個並行塊。設計


帶有for的制導指令:

for制導語句是將for循環分配給各個線程執行,這裏要求數據不存在依賴。

 使用形式爲:

(1)#pragma omp parallel for

         for()

(2)#pragma omp parallel

        {//注意:大括號必需要另起一行

         #pragma omp for

          for()

        }

注意:第二種形式中並行塊裏面不要再出現parallel制導指令,好比寫成這樣就不能夠:


#pragma omp parallel

        {

         #pragma omp parallel for

          for()

        }

第一種形式做用域只是緊跟着的那個for循環,而第二種形式在整個並行塊中能夠出現多個for制導指令。下面結合例子程序講解for循環並行化須要注意的地方。

 

  假如不使用for制導語句,而直接在for循環前使用parallel語句:(爲了使輸出不出現混亂,這裏使用printf代替cout)

#include <iostream>
#include <stdio.h>
#include "omp.h"
using namespace std;
int main(int argc, char **argv) {
//設置線程數,通常設置的線程數不超過CPU核心數,這裏開4個線程執行並行代碼段
omp_set_num_threads(4);
#pragma omp parallel
for (int i = 0; i < 2; i++)
//cout << "i = " << i << ", I am Thread " << omp_get_thread_num() << endl;
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
}
輸出結果爲:

i = 0, I am Thread 0
i = 0, I am Thread 1
i = 1, I am Thread 0
i = 1, I am Thread 1
i = 0, I am Thread 2
i = 1, I am Thread 2
i = 0, I am Thread 3
i = 1, I am Thread 3
從輸出結果能夠看到,若是不使用for制導語句,則每一個線程都執行整個for循環。因此,使用for制導語句將for循環拆分開來儘量平均地分配到各個線程執行。將並行代碼改爲這樣以後:

#pragma omp parallel for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
輸出結果爲:
i = 4, I am Thread 2
i = 2, I am Thread 1
i = 0, I am Thread 0
i = 1, I am Thread 0
i = 3, I am Thread 1
i = 5, I am Thread 3
能夠看到線程0執行i=0和1,線程1執行i=2和3,線程2執行i=4,線程3執行i=5。線程0就是主線程
這樣整個for循環被拆分並行執行了。上面的代碼中parallel和for連在一塊使用的,其只能做用到緊跟着的for循環,循環結束了並行塊就退出了。

上面的代碼能夠改爲這樣:

#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
}
這寫法和上面效果是同樣的。須要注意的問題來了:若是在parallel並行塊裏再出現parallel會怎麼樣呢?回答這個問題最好的方法就是跑一遍代碼看看,因此把代碼改爲這樣:
#pragma omp parallel
{
#pragma omp parallel for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
}
輸出結果:
i = 0, I am Thread 0
i = 0, I am Thread 0
i = 1, I am Thread 0
i = 1, I am Thread 0
i = 2, I am Thread 0
i = 2, I am Thread 0
i = 3, I am Thread 0
i = 3, I am Thread 0
i = 4, I am Thread 0
i = 4, I am Thread 0
i = 5, I am Thread 0
i = 5, I am Thread 0
i = 0, I am Thread 0
i = 1, I am Thread 0
i = 0, I am Thread 0
i = 2, I am Thread 0
i = 1, I am Thread 0
i = 3, I am Thread 0
i = 2, I am Thread 0
i = 4, I am Thread 0
i = 3, I am Thread 0
i = 5, I am Thread 0
i = 4, I am Thread 0
i = 5, I am Thread 0
能夠看到,只有一個線程0,也就是隻有主線程執行for循環,並且總共執行4次,每次都執行整個for循環!因此,這樣寫是不對的。


  固然,上面說的for制導語句的兩種寫法是有區別的,好比兩個for循環之間有一些代碼只能有一個線程執行,那麼用第一種寫法只須要這樣就能夠了:

#pragma omp parallel for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
//這裏是兩個for循環之間的代碼,將會由線程0即主線程執行
printf("I am Thread %d\n", omp_get_thread_num());
#pragma omp parallel for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
離開了for循環就剩主線程了,因此兩個循環間的代碼是由線程0執行的,輸出結果以下:
i = 0, I am Thread 0
i = 2, I am Thread 1
i = 1, I am Thread 0
i = 3, I am Thread 1
i = 4, I am Thread 2
i = 5, I am Thread 3
I am Thread 0
i = 4, I am Thread 2
i = 2, I am Thread 1
i = 5, I am Thread 3
i = 0, I am Thread 0
i = 3, I am Thread 1
i = 1, I am Thread 0
   可是若是用第二種寫法把for循環寫進parallel並行塊中就須要注意了!
   因爲用parallel標識的並行塊中每一行代碼都會被多個線程處理,因此若是想讓兩個for循環之間的代碼由一個線程執行的話就須要在代碼前用single或master制導語句標識,master由是主線程執行,single是選一個線程執行,這個到底選哪一個線程不肯定。因此上面代碼能夠寫成這樣:

#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
#pragma omp master
{
//這裏的代碼由主線程執行
printf("I am Thread %d\n", omp_get_thread_num());
}
#pragma omp for
for (int i = 0; i < 6; i++)
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
}
效果和上面的是同樣的,若是不指定讓主線程執行,那麼將master改爲single便可。
到這裏,parallel和for的用法都講清楚了。接下來就開始講並行處理時數據的同步問題,這是多線程編程裏都會遇到的一個問題。

 

   爲了講解數據同步問題,先由一個例子開始:

#include <iostream>
#include "omp.h"
using namespace std;
int main(int argc, char **argv) {
int n = 100000;
int sum = 0;
omp_set_num_threads(4);
#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < n; i++) {
{
sum += 1;
}
}
}
cout << " sum = " << sum << endl;
}
指望的正確結果是100000,可是這樣寫是錯誤的。看代碼,因爲默認狀況下sum變量是每一個線程共享的,因此多個線程同時對sum操做時就會由於數據同步問題致使結果不對,顯然,輸出結果每次都不一樣,這是沒法預知的,以下:
第一次輸出sum = 58544
第二次輸出sum = 77015
第三次輸出sum = 78423


  那麼,怎麼去解決這個數據同步問題呢?解決方法以下:
方法一:對操做共享變量的代碼段作同步標識

代碼修改以下:

#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < n; i++) {
{
#pragma omp critical
sum += 1;
}
}
}
cout << " sum = " << sum << endl;
  critical制導語句標識的下一行代碼,也能夠是跟着一個大括號括起來的代碼段作了同步處理。輸出結果100000

方法二:每一個線程拷貝一份sum變量,退出並行塊時再把各個線程的sum相加

並行代碼修改以下:

#pragma omp parallel
{
#pragma omp for reduction(+:sum)
for (int i = 0; i < n; i++) {
{
sum += 1;
}
}
}
reduction制導語句,操做是退出時將各自的sum相加存到外面的那個sum中,因此輸出結果就是100000啦~~

方法三:這種方法貌似不那麼優雅
代碼修改以下:

int n = 100000;
int sum[4] = { 0 };
omp_set_num_threads(4);
#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < n; i++) {
{
sum[omp_get_thread_num()] += 1;
}
}
}
cout << " sum = " << sum[0] + sum[1] + sum[2] + sum[3] << endl;
每一個線程操做的都是以各自線程id標識的數組位置,因此結果固然正確。

數據同步就講完了,上面的代碼中for循環是一個一個i平均分配給各個線程,若是想把循環一塊一塊分配給線程要怎麼作呢?這時候用到了schedule制導語句。下面的代碼演示了schedule的用法:

#include <iostream>
#include "omp.h"
#include <stdio.h>
using namespace std;
int main(int argc, char **argv) {
int n = 12;
omp_set_num_threads(4);
#pragma omp parallel
{
#pragma omp for schedule(static, 3)
for (int i = 0; i < n; i++) {
{
printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());
}
}
}
}
上面代碼中for循環並行化時將循環不少不少塊,每一塊大小爲3,而後再平均分配給各個線程執行。
輸出結果以下:

i = 6, I am Thread 2
i = 3, I am Thread 1
i = 7, I am Thread 2
i = 4, I am Thread 1
i = 8, I am Thread 2
i = 5, I am Thread 1
i = 0, I am Thread 0
i = 9, I am Thread 3
i = 1, I am Thread 0
i = 10, I am Thread 3
i = 2, I am Thread 0
i = 11, I am Thread 3
從輸出結果能夠看到:線程0執行i=0 1 2,線程1執行i=3 4 5,線程2執行i=6 7 8,線程3執行i=9 10 11,若是後面還有則又從線程0開始分配。


  OK,for循環並行化的知識基本講完了,還有一個有用的制導語句barrier,用它能夠在並行塊中設置一個路障,必須等待全部線程到達時才能經過,這個通常在並行處理循環先後存在依賴的任務時使用到。

  是否是很簡單?

相關文章
相關標籤/搜索