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

           {

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

            }

比如下面這段代碼:

#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,用它可以在並行塊中設置一個路障,必須等待所有線程到達時才能通過,這個一般在並行處理循環前後存在依賴的任務時使用到

  是不是很簡單?












 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章