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



    轉載請聲明出處http://blog.csdn.net/zhongkejingwang/article/details/40018735

    在C/C++中使用OpenMP優化代碼方便又簡單,代碼中需要並行處理的往往是一些比較耗時的for循環,所以重點介紹一下OpenMP中for循環的應用。個人感覺只要掌握了文中講的這些就足夠了,如果想要學習OpenMP可以到網上查查資料。

    工欲善其事,必先利其器。如果還沒有搭建好omp開發環境的可以看一下OpenMP並行程序設計——Eclipse開發環境的搭建

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

           #pragma omp parallel

           {

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

            }

比如下面這段代碼:

  1. #include <iostream>  
  2. #include "omp.h"  
  3. using namespace std;  
  4. int main(int argc, char **argv) {  
  5.     //設置線程數,一般設置的線程數不超過CPU核心數,這裏開4個線程執行並行代碼段  
  6.     omp_set_num_threads(4);  
  7. #pragma omp parallel  
  8.     {  
  9.         cout << "Hello" << ", I am Thread " << omp_get_thread_num() << endl;  
  10.     }  
  11. }  
#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號  

以上代碼執行結果爲:

  1. Hello, I am Thread 1  
  2. Hello, I am Thread 0  
  3. Hello, I am Thread 2  
  4. Hello, I am Thread 3  
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)

  1. #include <iostream>  
  2. #include <stdio.h>  
  3. #include "omp.h"  
  4. using namespace std;  
  5. int main(int argc, char **argv) {  
  6.     //設置線程數,一般設置的線程數不超過CPU核心數,這裏開4個線程執行並行代碼段  
  7.     omp_set_num_threads(4);  
  8. #pragma omp parallel  
  9.     for (int i = 0; i < 2; i++)  
  10.         //cout << "i = " << i << ", I am Thread " << omp_get_thread_num() << endl;  
  11.         printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  12. }  
#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());
}

輸出結果爲:

  1. i = 0, I am Thread 0  
  2. i = 0, I am Thread 1  
  3. i = 1, I am Thread 0  
  4. i = 1, I am Thread 1  
  5. i = 0, I am Thread 2  
  6. i = 1, I am Thread 2  
  7. i = 0, I am Thread 3  
  8. i = 1, I am Thread 3  
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循環拆分開來儘可能平均地分配到各個線程執行。將並行代碼改成這樣之後:

  1. #pragma omp parallel for  
  2.     for (int i = 0; i < 6; i++)  
  3.         printf("i = %d, I am Thread %d\n", i, 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());
輸出結果爲:

  1. i = 4, I am Thread 2  
  2. i = 2, I am Thread 1  
  3. i = 0, I am Thread 0  
  4. i = 1, I am Thread 0  
  5. i = 3, I am Thread 1  
  6. i = 5, I am Thread 3  
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循環,循環結束了並行塊就退出了。

上面的代碼可以改成這樣:

  1. #pragma omp parallel  
  2.     {  
  3. #pragma omp for  
  4.         for (int i = 0; i < 6; i++)  
  5.             printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  6.     }  
#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會怎麼樣呢?回答這個問題最好的方法就是跑一遍代碼看看,所以把代碼改成這樣:

  1. #pragma omp parallel  
  2.     {  
  3. #pragma omp parallel for  
  4.         for (int i = 0; i < 6; i++)  
  5.             printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  6.     }  
#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());
	}
輸出結果:

  1. i = 0, I am Thread 0  
  2. i = 0, I am Thread 0  
  3. i = 1, I am Thread 0  
  4. i = 1, I am Thread 0  
  5. i = 2, I am Thread 0  
  6. i = 2, I am Thread 0  
  7. i = 3, I am Thread 0  
  8. i = 3, I am Thread 0  
  9. i = 4, I am Thread 0  
  10. i = 4, I am Thread 0  
  11. i = 5, I am Thread 0  
  12. i = 5, I am Thread 0  
  13. i = 0, I am Thread 0  
  14. i = 1, I am Thread 0  
  15. i = 0, I am Thread 0  
  16. i = 2, I am Thread 0  
  17. i = 1, I am Thread 0  
  18. i = 3, I am Thread 0  
  19. i = 2, I am Thread 0  
  20. i = 4, I am Thread 0  
  21. i = 3, I am Thread 0  
  22. i = 5, I am Thread 0  
  23. i = 4, I am Thread 0  
  24. i = 5, I am Thread 0  
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循環之間有一些代碼只能有一個線程執行,那麼用第一種寫法只需要這樣就可以了:

  1. #pragma omp parallel for  
  2.     for (int i = 0; i < 6; i++)  
  3.         printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  4.     //這裏是兩個for循環之間的代碼,將會由線程0即主線程執行  
  5.     printf("I am Thread %d\n", omp_get_thread_num());  
  6. #pragma omp parallel for  
  7.     for (int i = 0; i < 6; i++)  
  8.         printf("i = %d, I am Thread %d\n", i, 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即主線程執行
	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執行的,輸出結果如下:

  1. i = 0, I am Thread 0  
  2. i = 2, I am Thread 1  
  3. i = 1, I am Thread 0  
  4. i = 3, I am Thread 1  
  5. i = 4, I am Thread 2  
  6. i = 5, I am Thread 3  
  7. I am Thread 0  
  8. i = 4, I am Thread 2  
  9. i = 2, I am Thread 1  
  10. i = 5, I am Thread 3  
  11. i = 0, I am Thread 0  
  12. i = 3, I am Thread 1  
  13. i = 1, I am Thread 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是選一個線程執行,這個到底選哪個線程不確定。所以上面代碼可以寫成這樣:

  1. #pragma omp parallel  
  2.     {  
  3. #pragma omp for  
  4.         for (int i = 0; i < 6; i++)  
  5.             printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  6. #pragma omp master  
  7.         {  
  8.             //這裏的代碼由主線程執行  
  9.             printf("I am Thread %d\n", omp_get_thread_num());  
  10.         }  
  11. #pragma omp for  
  12.         for (int i = 0; i < 6; i++)  
  13.             printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  14.     }  
#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的用法都講清楚了。接下來就開始講並行處理時數據的同步問題,這是多線程編程裏都會遇到的一個問題。


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

  1. #include <iostream>  
  2. #include "omp.h"  
  3. using namespace std;  
  4. int main(int argc, char **argv) {  
  5.     int n = 100000;  
  6.     int sum = 0;  
  7.     omp_set_num_threads(4);  
  8. #pragma omp parallel  
  9.     {  
  10. #pragma omp for  
  11.         for (int i = 0; i < n; i++) {  
  12.             {  
  13.                 sum += 1;  
  14.             }  
  15.         }  
  16.     }  
  17.     cout << " sum = " << sum << endl;  
  18. }  
#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操作時就會因爲數據同步問題導致結果不對,顯然,輸出結果每次都不同,這是無法預知的,如下:

  1. 第一次輸出sum = 58544  
  2. 第二次輸出sum = 77015  
  3. 第三次輸出sum = 78423  
第一次輸出sum = 58544
第二次輸出sum = 77015
第三次輸出sum = 78423


  那麼,怎麼去解決這個數據同步問題呢?解決方法如下:

方法一:對操作共享變量的代碼段做同步標識

代碼修改如下:

  1. #pragma omp parallel  
  2.     {  
  3. #pragma omp for  
  4.         for (int i = 0; i < n; i++) {  
  5.             {  
  6. #pragma omp critical  
  7.                 sum += 1;  
  8.             }  
  9.         }  
  10.     }  
  11.     cout << " sum = " << sum << endl;  
#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相加

並行代碼修改如下:

  1. #pragma omp parallel  
  2.     {  
  3. #pragma omp for reduction(+:sum)  
  4.         for (int i = 0; i < n; i++) {  
  5.             {  
  6.                 sum += 1;  
  7.             }  
  8.         }  
  9.     }  
#pragma omp parallel
	{
#pragma omp for reduction(+:sum)
		for (int i = 0; i < n; i++) {
			{
				sum += 1;
			}
		}
	}
reduction制導語句,操作是退出時將各自的sum相加存到外面的那個sum中,所以輸出結果就是100000啦~~

方法三:這種方法貌似不那麼優雅

代碼修改如下:

  1. int n = 100000;  
  2.     int sum[4] = { 0 };  
  3.     omp_set_num_threads(4);  
  4. #pragma omp parallel  
  5.     {  
  6. #pragma omp for  
  7.         for (int i = 0; i < n; i++) {  
  8.             {  
  9.                 sum[omp_get_thread_num()] += 1;  
  10.             }  
  11.         }  
  12.     }  
  13.     cout << " sum = " << sum[0] + sum[1] + sum[2] + sum[3] << endl;  
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的用法:

  1. #include <iostream>  
  2. #include "omp.h"  
  3. #include <stdio.h>  
  4. using namespace std;  
  5. int main(int argc, char **argv) {  
  6.     int n = 12;  
  7.     omp_set_num_threads(4);  
  8. #pragma omp parallel  
  9.     {  
  10. #pragma omp for schedule(static, 3)  
  11.         for (int i = 0; i < n; i++) {  
  12.             {  
  13.                 printf("i = %d, I am Thread %d\n", i, omp_get_thread_num());  
  14.             }  
  15.         }  
  16.     }  
  17. }  
#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,然後再平均分配給各個線程執行。

輸出結果如下:

  1. i = 6, I am Thread 2  
  2. i = 3, I am Thread 1  
  3. i = 7, I am Thread 2  
  4. i = 4, I am Thread 1  
  5. i = 8, I am Thread 2  
  6. i = 5, I am Thread 1  
  7. i = 0, I am Thread 0  
  8. i = 9, I am Thread 3  
  9. i = 1, I am Thread 0  
  10. i = 10, I am Thread 3  
  11. i = 2, I am Thread 0  
  12. i = 11, I am Thread 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,用它可以在並行塊中設置一個路障,必須等待所有線程到達時才能通過,這個一般在並行處理循環前後存在依賴的任務時使用到

  是不是很簡單?


發佈了24 篇原創文章 · 獲贊 51 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章