1. fork/join 並列実行モードの概念
OpenMPは、主に共有ストレージ・コンピューター上で並列プログラミングを行うために設計された、コンパイラー命令とライブラリー関数のコレクションです。
前回の記事では、OpenMPのParallel for命令の1つを試してみました。前回の記事でお分かりのように、OpenMPの並列実行は、コードの非並列部分が実行される前に終了しなければなりません。これは、標準的な並列モードであるフォーク/結合並列であり、共有ストレージ並列プログラムが使用するものです。
標準的な並列モードによるコード実行の基本的な考え方は、プログラムの最初にメインスレッドが1つだけあり、プログラムのシリアル部分はメインスレッドによって実行され、並列部分は他のスレッドをスポーンして実行されますが、前の投稿の次のコードのように、並列部分が終了していなければシリアル部分は実行されません:
int main(int argc, char* argv[])
{
clock_t t1 = clock();
#pragma omp parallel for
for ( int j = 0; j < 2; j++ ){
test();
}
clock_t t2 = clock();
printf("Total time = %d/n", t2-t1);
test();
return 0;
}
2.OpenMPコマンドとライブラリ関数の紹介
OpenMPの基本的なコマンドと、一般的なコマンドの使い方を説明します。
C/C++では、OpenMPディレクティブは次の形式を使用します。
#pragma omp directive [clause [節]...].].
前述したparallel forはディレクティブであり、OpenMPの「ディレクティブ」を「コンパイル・ガイドライン・ステートメント」と呼ぶ書籍もあり、その後にオプションの条項が続きます。例えば、以下の節は省略可能です:
#pragma omp parallel private(i, j)
パラレルは指示、プライベートは条項。
便宜上、#pragmaディレクティブとOpenMPディレクティブを含む行をステートメントと呼び、上の行のように並列ステートメントと呼びます。
OpenMPコマンドの一部を以下に示します:
parallelはコードセグメントで使用され、コードが複数のスレッドで並列に実行されることを示します。
forループで使用されるforは、並列実行のためにループを複数のスレッドに割り当てます。
parallelとfor文を組み合わせたparallel forは、forループの中で、forループのコードが複数のスレッドで並列に実行されることを示すためにも使われます。
セクションは、並列に実行されるコードセグメントで使用されます。
並列セクション、並列ステートメントとセクションステートメントの組み合わせ
クリティカルゾーンで使用されます。
単一のスレッドによってのみ実行されるコードセグメントで使用されるsingleは、次のコードセグメントが単一のスレッドによって実行されることを意味します。
フラッシュ。
バリアは、並列ゾーンのコードのスレッド同期に使用され、すべてのスレッドがバリアまで実行されるまで、バリアへのすべてのスレッドの実行が停止します。
atomicは、ブレーキによって更新されるメモリ領域を指定するために使用されます。
マスターは、メイン・スレッドが実行するコード・ブロックを指定するために使われます。
orderedは、並列領域内のループの実行順序を指定するために使用されます。
threadprivate は、変数がスレッドプライベートであることを指定するために使用します。
上記のコマンドに加えて、OpenMPには多くのライブラリ関数があります。以下に、よく使用されるライブラリ関数をいくつか示します:
omp_get_num_procs は、このスレッドを実行しているマルチプロセッサのプロセッサ数を返します。
omp_get_num_threads は、現在の並列領域でアクティブなスレッドの数を返します。
omp_get_thread_num, スレッド番号を返します。
omp_set_num_threads : コードの並列実行時のスレッド数を設定します。
omp_init_lock, シンプルなロックの初期化
ロック操作
omp_unset_lock、omp_set_lock関数と対になるロック解除操作。
omp_init_lock関数のペア演算関数であるomp_destroy_lockは、ロックを閉じます。
OpenMPの条項には次のようなものがあります。
private は、各スレッドが変数のプライベートコピーを持つことを指定します。
firstprivateは、各スレッドが変数のプライベートコピーを持ち、その変数がメインスレッドの初期値から継承されることを指定します。
lastprivateは、主にスレッド内のプライベート変数の値が、並列処理の終了時にメインスレッド内の対応する変数にコピーバックされることを指定するために使用されます。
reduceは、1 つまたは複数の変数がプライベートであること、および並列処理の最後にこれらの変数が指定された処理を実行することを指定するために使用します。
nowaitは、仕様で暗示されているwaitを無視します。
num_threads(スレッド数
scheduleは、forループの繰り返しをどのようにスケジュールするかを指定します。
複数のスレッド間で共有される変数を指定します。
orderedは、for ループを順番に実行することを指定します。
copyprivateは、指定された変数を複数のスレッドの共有変数にするために単一のディレクティブで使用されます。
copyinは、スレッドプライベート変数の値をメインスレッドの値で初期化することを指定するために使用します。
並列処理領域の変数をどのように使用するかを指定します。
3.パラレルコマンドの使い方
parallelは並列ブロックを構築するために使用され、forやsectionなどの他の命令も一緒に使用できます。
C/C++では、並列は次のように使われます:
#pragma omp parallel [for | sections] [ [ ] ]
{
//
}
parallelステートメントの後には中括弧が続き、並列実行されるコードを囲む。
void main(int argc, char *argv[]) {
#pragma omp parallel
{
printf("こんにちは, World!/n”);
}
}
上のコードを実行すると、次のような結果が出力される。
Hello, World!
Hello, World!
Hello, World!
Hello, World!
parallel文のコードが4回実行されているのがわかるだろう。これは、parallel文のコードを実行するために、合計4つのスレッドが作成されていることを意味する。
また、実行に使用するスレッド数を指定することもできる。_threads句である:
void main(int argc, char *argv[]) {
#pragma omp parallel num_threads(8)
{
printf("こんにちは, World!, ThreadId=%d/n”, omp_get_thread_num() );
}
}
上のコードを実行すると、次のような結果が出力される:
Hello, World!, ThreadId = 2
Hello, World!, ThreadId = 6
Hello, World!, ThreadId = 4
Hello, World!, ThreadId = 0
Hello, World!, ThreadId = 5
Hello, World!, ThreadId = 7
Hello, World!, ThreadId = 1
Hello, World!, ThreadId = 3
ThreadIdの違いは、上記のコードを実行するために8つのスレッドが作成されていることを示しています。つまり、parallelディレクティブは1つのコードに対して複数のスレッドを作成して実行するために使用されます。
従来のスレッド作成関数と比較すると、スレッドを作成するスレッドエントリー関数に対してスレッド作成関数を繰り返し呼び出し、スレッドの実行が終了するのを待つのと同じことです。
4.指導のための使用方法
for指示文は、forループを複数のスレッドに分散させるために使用します。for指示文は、parallel指示文と組み合わせてparallel for指示文とすることもできますし、単独でparallel文のparallelブロックの中で使用することもできます。
#pragma omp [parallel] for [clause].
forループ文
まず、for文が単独で使われた場合の効果を見てみましょう:
for文だけを使うとどうなるか見てみよう:
int j = 0;
#pragma omp for
for ( j = 0; j < 4; j++ ){
printf( = %d, ThreadId = %d/n”, j, omp_get_thread_num());
}
上のコードを実行すると、次のような結果が出力される。
j = 0, ThreadId = 0
j = 1, ThreadId = 0
j = 2, ThreadId = 0
j = 3, ThreadId = 0
結果を見ればわかるように、4つのループはすべて1つのスレッドで実行されるため、for命令はparallel命令と組み合わせて使うと効果的だ:
如以下代码就是parallel 和for一起结合成parallel for的形式使用的:
int j = 0;
#pragma omp parallel for
for ( j = 0; j < 4; j++ ){
printf( = %d, ThreadId = %d/n”, j, omp_get_thread_num());
}
実行後、次のような結果が出力される:
j = 0, ThreadId = 0
j = 2, ThreadId = 2
j = 1, ThreadId = 1
j = 3, ThreadId = 3
ループが4つの異なるスレッドに割り当てられているのがわかるだろう。
上記のコードは、次のように書き換えることもできる:
int j = 0;
#pragma omp parallel
{
#pragma omp for
for ( j = 0; j < 4; j++ ){
printf( = %d, ThreadId = %d/n”, j, omp_get_thread_num());
}
}
上記のコードを実行すると、次のような結果が出力されます:
j = 1, ThreadId = 1
j = 3, ThreadId = 3
j = 2, ThreadId = 2
j = 0, ThreadId = 0
また、例えば並列ブロック内に複数のfor文を記述することも可能です:
int j;
#pragma omp parallel
{
#pragma omp for
for ( j = 0; j < 100; j++ ){
}
#pragma omp for
for ( j = 0; j < 100; j++ ){
}
}
forループ文は、ある仕様に従って記述する必要があります。つまり、forループの括弧内の文は、ある仕様に従って記述する必要があります。
for( i=start; i < end; i++)
i=start;はforループの最初の文で、「変数=初期値」と書かなければなりません。i=0の場合
i < end;はforループの2番目の文で、次の4つの形式のうちの1つで書くことができます:
変数 < 境界値
変数 <= 境界値
変数 > 境界値
変数 >= 境界値
例:i>10 i<10i>=10 i>10など。
最後の文 i++ は、次の9通りの書き方があります。
i++
++i
i--
--i
i += inc
i -= inc
ii = i + inc
i = inc + i
ii = i -incは
例えば、i += 2; i -= 2; i = i + 2; i = i - 2; はすべて仕様に準拠して書かれています。
5.セクションとセクション指示文の使用法
section文は、section文のコードを複数の異なるセクションに分割し、それぞれを並列に実行するために使用します。使い方は以下のようになります:
#pragma omp [parallel] sections [ ]
{
#pragma omp section
{
コードブロック
}
}
まず、次のコード例を見てください:
void main(int argc, char *argv)
{
#pragma omp parallel sections {
#pragma omp section
printf(“section 1 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
printf(“section 2 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
printf(“section 3 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
printf(“section 4 ThreadId = %d/n”, omp_get_thread_num());
}
実行後、以下の結果が出力されます:
セクション 1 ThreadId = 0
セクション 2 ThreadId = 2
第4節 ThreadId = 3
第3節 ThreadId = 1
この結果から、セクション4のコードはセクション3のコードよりも早く実行されていることがわかります。これは、各セクションのコードが並列に実行され、各セクションが異なるスレッドに割り当てられて実行されていることを意味します。
セクションステートメントを使用する場合、このアプローチでは、各セクションのコードの実行時間が異なりすぎないようにする必要があることに注意する必要があります。
上記のコードは次のように書き換えることもできます:
void main(int argc, char *argv)
{
#pragma omp parallel {
#pragma omp sections
{
#pragma omp section
printf(“section 1 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
printf(“section 2 ThreadId = %d/n”, omp_get_thread_num());
}
#pragma omp sections
{
#pragma omp section
printf(“section 3 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
printf(“section 4 ThreadId = %d/n”, omp_get_thread_num());
}
}
実行後、以下の結果が出力されます:
セクション 1 ThreadId = 0
セクション 2 ThreadId = 3
セクション 3 ThreadId = 3
第4節 ThreadId = 1
つまり、2番目のセクションのコードは、1番目のセクションのコードが実行されるまで実行することができません。
for文による振り分けはシステムによって自動的に行われ、各ループの間に時間差がない限り、振り分けは非常に均等です。スレッドを分割するためにセクションを使用することは、スレッドを分割する手動方法であり、最終的に並列性はプログラマに依存しなければなりません。
この記事では、スレッド生成関数を呼び出すよりも便利で効率的な方法でスレッドを生成するために、OpenMPディレクティブのparallel、for、section、sectionを実際に使用します。