2011年4月26日 星期二

OpenMP 心得 (六) Single Construct and Master Construct

在平行執行程式時有些工作其實只要由一個執行緒來做即可,例如印出訊息到螢幕或者是讀寫檔案資料等,像這類只需要指派一個執行緒來做的工作我們可以將它們放到 single construct 或 master construct 之中。這兩個 constructs 的功用看起來雖然相同,但在操控執行緒的行為上卻有些不一樣,在這篇文章中我們將討論這兩個 constructs 的用法以及在什麼情況下該採用那個 construct。

Single Construct:
Single construct 在分類上屬於 work-sharing constructs 的一員,因此它的結束點有一個隱性的執行緒同步點存在。在平行區域中,第一個遭遇到 single construct 的執行緒將被指派去執行 construct 內的工作,而其它的執行緒則停在 construct 的結束點直到負責執行 single construct 的執行緒完成工作後才能一起去處理接下來的任務。下圖說明執行緒在 single construct 的工作流程,圖中 TASK 2 位於 single construct 內,因此系統將指派第一個到達這個構造的執行緒去處理 TASK 2:
在 C/C++ 中使用 single construct 的語法如下:
#pragma omp single [clause[[,] clause]...]
    structured block
在 Fortran 的語法如下:
!$omp single [clause[[,] clause]...]
       structured block
!$omp end single [nowait, [copyprivate]]

Master Construct:
Master construct 與 single construct 在操控執行緒的行為上只有兩個地方不同:一是 master construct 保證在構造內的程式只由 master thread (TID = 0) 來執行;二是 master construct 的結束點並沒有隱性的執行緒同步點,所以當主執行緒正在處理 master construct 內的工作時,其它的執行緒會逕自去執行其它的工作。關於 master construct 的動作流程可以使用下圖來說明,注意在 master construct 內的 TASK 2 一定是由主執行緒 (藍色箭條線) 所執行,同一時間其它執行緒會略過 TASK 2 而去處理 TASK 3:
Master construct 在 C/C++ 語法如下:
#pragma omp master
    structured block
在 Fortran 的語法如下:
!$omp master
       structured block
!$omp end master

使用時機:
Single construct 與 master construct 在使用上都必須嵌入在 parallel construct 之中。另外,雖然 single construct 或 master construct 都可以達到在平行區域中將任務委由單一執行緒處理的要求,但因它們在控制執行緒行為上的差異造成我們在選用這兩種 constructs 時會有些限制與考量。一般選用的準則如下:
  • 1. 當我們並不在乎構造內的工作該由那一個執行緒去處理,但希望未被指派到的執行緒可以等待該工作完成後再去進行其它任務時宜優先採用 single construct。
  • 2. 當只由單一執行緒處理的工作結果不會影響到平行區域內接下來要被執行的任務時宜選用 master construct。
對於第二項準則要特別注意,因為 master construct 的結尾沒有隱性的執行緒同步點,所以當主執行緒在處理 master construct 的工作期間,其它的執行緒也正在進行各自的工作,如果這些工作必須引用到由 master construct 內部某些指令所產生的結果,但主執行緒卻尚未完成它們時就會發生程式邏輯錯誤的狀況。為了闡述這個錯誤狀況,以下我們設計一個簡單的程式來做說明。程式的意圖很簡單,分為下列三個步驟:
  • 1. 在序列執行區將陣列變數 a 的所有元素初值設定為 999。
  • 2. 在平行區域內讓單一執行緒對 a 所有的元素重新賦值。
  • 3. 使用 loop construct 讓個別執行緒將分配到的陣列元素新值輸出在螢幕上。
在第一個範例程式中我們將採用 single construct 來處理上述步驟 2 的任務,完整的 C 程式碼如下:
//--Program: omp_single_initialized.c
//--Example of OpenMP Single Construct
//--Written by AAZ

#include <stdio.h>
#include <omp.h>

#define NUM 8

int main(int argc, char **argv)
{
    int i, tid, a[NUM];

    for (i = 0; i < NUM; i++)
        a[i] = 999;

    #pragma omp parallel private(tid)
    {
    tid = omp_get_thread_num();

    #pragma omp single
    {
    printf("---TID %d: initialize a[i] ...\n", tid);
    for (i = 0; i < NUM; i++) {
        a[i] = i;
    }
    printf("---TID %d: a[i] has been initialized.\n", tid);
    }  //--End of OMP Single Construct
    
    #pragma omp for 
    for (i = 0; i < NUM; i++)
        printf("TID %d: a[%d] = %d\n", tid, i, a[i]);

    }  //--End of OMP Parallel Construct
    return 0;
}
以下為使用 4 個執行緒執行程式的結果:
由上圖可看出賦予 a 陣列所有元素新值的工作是由 TID = 3 的執行緒來負責執行,且在完成這項工作後所有執行緒才一起進入步驟 3 中印出 a 陣列的元素值。整個程式的執行結果與我們的預期結果相符,即陣列變數 a 在進入步驟 3 前已完成所有元素值的更新。

接下來我們來看採用 master construct 進行步驟 2 的情形,完整 C 程式碼與執行結果如下所示:
//--Program: omp_master_initialized.c
//--Example of OpenMP Master Construct
//--Written by AAZ

#include <stdio.h>
#include <omp.h>

#define NUM 8

int main(int argc, char **argv)
{
    int i, tid, a[NUM];

    for (i = 0; i < NUM; i++)
        a[i] = 999;

    #pragma omp parallel private(tid)
    {
    tid = omp_get_thread_num();

    #pragma omp master
    {
    printf("---TID %d: initialize a[i] ...\n", tid);
    for (i = 0; i < NUM; i++) {
        a[i] = i;
    }
    printf("---TID %d: a[i] has been initialized.\n", tid);
    }  //--End of OMP Master Construct
    
    #pragma omp for 
    for (i = 0; i < NUM; i++)
        printf("TID %d: a[%d] = %d\n", tid, i, a[i]);

    }  //--End of OMP Parallel Construct
    return 0;
}
使用 4 個執行緒執行程式的結果:
從上圖可以看出程式步驟 2 是由主執行緒 (TID = 0) 所執行,而在主執行緒對 a 陣列所有元素完成重新賦值之前,其它執行緒卻已先進入到程式的第三個步驟將未被更新的元素值印出,造成執行結果與我們預期結果不符的現象。由這個試驗可以了解到使用 master construct 必須留意在主執行緒完成 master construct 內的工作之前是否有其它的執行緒會引用到這些工作的結果,如果有可能會發生這樣的事情則必須在 master construct 之後立即加上 barrier construct 以建立一個顯性的執行緒同步點來強制所有執行緒等待主執行緒完成工作。或者是直接使用 single construct 來取代 master construct,如果我們並不在乎到底是由那一個執行緒來完成需要由單一執行緒來做的工作。附帶說明一點,上圖的執行結果是經過刻意挑選以突顯我們要討論的問題,實際上的執行結果每次都不一樣,也有可能會出現符合我們預期的正確結果,但這是因為我們的範例程式過於簡單的關係,基本上在這樣的狀況下使用 master construct 所產生的結果都是不可信任的。

總結:
  • 1. 在平行區域中位於 single construct 與 master construct 內的程式將只由一個執行緒處理。
  • 2. Single construct 內的工作由第一個遭遇到它的執行緒負責處理,且在 single construct 結束處有一個隱性的執行緒同步點存在。
  • 3. Master construct 內的工作保證是由主執行緒 (TID = 0) 負責處理,而在 master construct 結束處並不存在隱性的執行緒同步點。
  • 4. 選用 single construct 或 master construct 時必須考量到位於 construct 內的工作結果是否會在未完成前被其它的執行緒不當地引用,如果有這樣的疑慮就必須採用 single construct,或使用在結束處加上 barrier construct 的 master construct。
關於上述第四點,我們可以瞭解在 master construct 之後加上 barrier construct 幾乎完全等效於 single construct,除了執行 construct 的執行緒是否被強制成主執行緒這一點之外。同樣地,在建構 single construct 時加上 nowait clause 後其效果也幾乎等同於 master construct。雖然我們可以靠加上一些條件讓這兩個 constructs 近乎相同,但理論上純粹的 single construct 比起加上 barrier construct 的 master construct 要來得有效率,而不加料的 master construct 也執行得比加上 nowait clause 的 single construct 來得快。因此除非我們真的很在意是否由主執行緒去處理 construct 內的工作,否則應盡量依需求來選用單純的 single construct 或者是 master construct。

(發佈日期:2011/04/26 by AAZ)

沒有留言:

張貼留言