プロセスとスレッド
OpenMPは、1つのプロセス、複数のスレッド(マルチスレッド)という環境で動くプログラムを書くための言語拡張です。
OpenMPでプログラムを書くために、まずプロセスとスレッドについて説明します。
1つのプロセスと複数のスレッドで動くプログラムの実行環境は、次の図のようになります。
スレッドとは
スレッドは、プログラムを動かすプロセッサコア(CPUコア)に対応します。
1つのスレッドは、動作中に1つのプロセッサコアを使います。
例えば、2つのプロセッサコアを使いたいのであれば、少なくとも2つのスレッドが必要になります。
なお、スレッドは一時停止状態にすることもできます。
プロセスとは
プロセスは、プログラムを動かすためにOSが用意した環境です。
プロセスは1つ以上のスレッドと、メモリやファイルといったリソースを持ちます。
すべてのスレッドは、メモリやファイルといったプロセスのリソースを共有します。
プロセッサコアとスレッド
スレッドは、OSが決めたタイミングでプロセッサコアに割り当てられ、実行されます。
プロセッサコアの数とスレッドの数は、一致していなくてもかまいません。
動作状態を持つスレッドの数がプロセッサコアの数よりも多いなら、
OSは数ミリ秒〜数十ミリ秒程度でスレッドを切り替えながら実行します。
結果として、プロセッサコアの数によらず、すべてのスレッドが同時に動作しているように見えます。
並列実行とリソースの競合
C/C++言語のプログラムは、OpenMPなどを使わない限り、
main関数を上から順に実行していきます。
このようなプログラムは1つのプロセス、1つのスレッドで動いています。
一方、OpenMPなどを使い複数のスレッドで動くプログラムは、
あるプログラムコードが動いている間に、別のプログラムコードも並列に実行しています。
メモリやファイルといったプログラムコード以外のリソースは、
スレッドごとに別のリソースを割り当てない限り、共有されます。
そのため、同一のリソースを複数のスレッドでアクセスする際には、
他のスレッドで使うリソースと競合しないように、細心の注意が必要になります。
以下、いくつかのケースについて説明します。
異なるメモリからの入力、異なるメモリへの出力
スレッドごとに別々のデータを入力し、スレッドごとに別々の処理結果を出力する場合は、
リソース(共有メモリ)の競合は起こりません。この場合、競合対策は必要ありません。
同一メモリからの入力、異なるメモリへの出力
複数のスレッドで同一のデータを入力し、スレッドごとに別々の処理結果を出力する場合は、
同じリソース(共有メモリ)への読み込みが発生します。
この場合、並列実行中に共有メモリの入力データは変更されませんので、
どのタイミングで読み込んでも結果が変わることはありません。
この場合も、特別な競合対策は必要ありません。
異なるメモリからの入力、同一メモリへの出力
スレッドごとに別々のデータを入力し、複数のスレッドで同一メモリへ処理結果を出力する場合は、
この場合、スレッド1が先に共有メモリに結果を出力した場合と、
スレッド2が先に共有メモリに結果を出力した場合で結果が変わる可能性があります。
この場合は、競合対策を行う必要があります。
競合するアクセスはいつ起こるのか
いくつかのケースについて説明しましたが、
スレッドが共有リソースの内容を変更する可能性がある場合には、
共有リソースへのアクセスが競合する可能性があります。
競合対策が行われているか不明なライブラリを利用する場合など、
共有リソースの内容を変更するか不明の場合は競合対策を行う必要があります。
具体例をいくつか示します。
あるスレッドが書いた結果を別のスレッドが読み込む場合。変数という共有リソースが変更されます。
ある変数の結果を更新する場合。あるスレッドが書いた結果を別のスレッドが読み込む場合と同じことをしています。
同一のファイルやストリームから読み込む場合。ストリーム内の位置という共有リソースが変更されます。
Cライブラリのstdoutやstderr、C++ライブラリのstd::coutやstd::cerr等に出力する場合。ストリームという共有リソースが変更されます。
リソースアクセスの同期
リソースへのアクセスに競合が生じる場合は、
アクセスするスレッドを待たせることで競合を解消できます。
リソースにアクセスするスレッドを待たせる方法を同期と呼びます。
同期とはスレッドを待たせることであり、そのぶん並列化の効果は落ちます。
並列化の効果を高めるには、同期を必要最低限に抑える必要があります。
バリア
同期をとる方法の1つは、
すべてのスレッドがその場所に進むまで処理を一時停止させる方法です。
同期をとるポイントをバリア(barrier)と呼びます。
スレッド1が書いた結果をスレッド2が読み込む場合は、
スレッド1が書き終わるまでスレッド2を待たせる必要があります。
この場合、スレッド1の書きこみ終了時点とスレッド2の読み込み開始時点の間にバリアを入れれば、
同期をとれることになります。
バリアを使うと、容易に同期をとることができます。
しかしながら、バリアはすべてのスレッドを待たせますので、
必要以上にスレッドを待たせてしまい、並列化の効果を落とすことがあります。
クリティカルセクション
バリアを使うと、あるリソースへの同時アクセスを防ぐために、
そのリソースにアクセスしないスレッドまで待たせることになります。
同期によりスレッドが待つ時間を減らすには、
リソース単位で別々の同期をとる必要があります。
クリティカルセクション(critical section)は、
リソースにアクセスする範囲のみをリソース単位の同期で保護することにより、
スレッドが待つ時間を減らす方法です。
クリティカルセクションは次のことを行います。
時間 | リソースの状態 | 同期処理の内容 |
入る時点 | 未使用 | リソースの状態を使用中にします。 |
入る時点 | 使用中 | スレッドを同期待ちにします。動作中に戻されたら、リソースの状態のチェックに戻ります。 |
抜ける時点 | (自身が使用中) | リソースの状態を未使用にします。同期待ち中のスレッドがあれば動作中に戻します。 |
クリティカルセクションでは、リソースの状態が未使用であれば、同期は行われません。
また、リソースの状態が使用中であっても、そのリソースを使用しないスレッドでは、同期は行われません。
クリティカルセクションはバリアと比べ、同期によりスレッドが待つ時間を減らせる可能性が高くなります。
クリティカルセクションを使ったプログラムの流れは次の図のようになります。
なお、クリティカルセクションが使う同期待ち用の機構は、ミューテックス(mutex)と呼ばれます。
ミューテックスは、日本語で排他制御を意味します。
マルチスレッドの開始と終了
データを複数のスレッドで処理する場合は、
まずマルチスレッドで実行するためのデータを準備し、
次にマルチスレッドでそれらのデータを処理し、
最後に結果をまとめるという流れが多くなります。
この場合、準備する時点でのデータはすべてのスレッドの入力となり、
最後に結果をまとめる際はすべてのスレッドの出力が必要になります。
この場合は、次の図のように前後にバリアを置くと良いでしょう。
このように、スレッドを作成したり終了したりする際にも、
同期が必要となることに注意してください。
関連ページ
OpenMPの解説 目次
次の項目: マルチスレッドプログラミング