OpenMP 同期制御指令:critical、atomic、flush の実践的使い分け

critical ディレクティブ:名前付き・無名の排他制御

#pragma omp critical は、複数スレッドが同時に実行される環境において、特定のコードブロック(臨界区)を**排他的に実行**するための仕組みです。共有変数への書き込みや状態依存の処理など、競合(race condition)を引き起こす可能性のある操作を保護します。

オプションで名前を指定できます:#pragma omp critical (section_a)。名前付き critical は、異なる名前を持つブロック同士は互いに干渉せず、並列実行が可能です。一方、名前を省略した場合は、すべての無名 critical ブロックが**同一のグローバルミューテックス**として扱われ、完全に直列化されます。

以下は、名前付きと無名の挙動を比較するサンプルです:

#include <iostream>
#include <omp.h>

int main() {
    #pragma omp parallel num_threads(4)
    {
        int tid = omp_get_thread_num();
        
        // 無名 critical:全スレッドがこのブロックで直列化
        #pragma omp critical
        {
            std::cout << "[A] thread " << tid << " entered\n";
        }
        
        // 名前付き critical:別名なら並列実行可能
        #pragma omp critical (first)
        {
            std::cout << "[B1] thread " << tid << " in 'first'\n";
        }
        
        #pragma omp critical (second)
        {
            std::cout << "[B2] thread " << tid << " in 'second'\n";
        }
    }
    return 0;
}

atomic ディレクティブ:軽量な原子更新

#pragma omp atomic は、単一のメモリアクセス(読み取り+書き込み+保存)を**アトミックに保証**するためのディレクティブです。critical よりも低コストで、主に配列要素やスカラー変数のインクリメント/デクリメント、累算(+=, &= など)に適しています。

制約として、対象となる文は以下のいずれかの形式でなければなりません:

  • x += expr;binop+, -, *, /, &, ^, <<, >>
  • x++, ++x, x--, --x

下記の例では、各スレッドが配列 counts の異なるインデックスを更新しており、atomic を用いることで競合を回避しつつ、並列性を維持しています:

#include <iostream>
#include <vector>
#include <omp.h>

int main() {
    std::vector<int> counts(5, 0);
    std::vector<int> indices = {0, 2, 1, 3, 4, 0, 2, 1};

    #pragma omp parallel for
    for (size_t i = 0; i < indices.size(); ++i) {
        int idx = indices[i];
        #pragma omp atomic
        counts[idx]++;
    }

    for (int v : counts) std::cout << v << " ";
    std::cout << "\n"; // 例: 2 2 2 1 1
    return 0;
}

flush ディレクティブ:キャッシュ整合性の明示的保証

OpenMP 実行環境では、各スレッドが独自のキャッシュ(ローカルビュー)を持つため、あるスレッドによる共有変数の更新が他のスレッドに即座に反映されないことがあります。#pragma omp flush は、そのような非可視性を解消するために、指定された変数の値を**強制的にメインメモリへ書き出し/読み込み**する同期点を設けます。

引数に変数リストを指定すると、その変数のみが対象になります(例:#pragma omp flush (flag, counter))。引数なしの #pragma omp flush は、すべての共有変数を対象としますが、過剰な使用はパフォーマンス低下を招くため、必要な箇所に限定すべきです。

次のコードでは、スレッド5がフラグ ready を true に設定し、他のスレッドがその値を確実に読み取れるよう、flush を活用しています:

#include <iostream>
#include <vector>
#include <omp.h>

int main() {
    bool ready = false;
    std::vector<int> results(8);

    #pragma omp parallel num_threads(8)
    {
        int tid = omp_get_thread_num();

        if (tid == 5) {
            #pragma omp flush (ready)
            ready = true;
            #pragma omp flush (ready) // 書き込み後、他のスレッドに可視化
        } else {
            #pragma omp flush (ready)
            results[tid] = ready ? 1 : 0;
        }
    }

    for (size_t i = 0; i < results.size(); ++i) {
        std::cout << "thread " << i << ": " << results[i] << "\n";
    }
    return 0;
}

タグ: OpenMP critical atomic flush parallel-programming

6月21日 20:17 投稿