インテル® C++ コンパイラー 17.0 デベロッパー・ガイドおよびリファレンス
ストランド境界を越えるロックの保持を回避することは、最も確実で簡単な方法です。同じレベルのストランドは同じロックを使用することができますが、親と子が同じロックを共有すると、次のような潜在的な問題が起こります。
cilk_spawn または cilk_sync 境界の後に作成されたストランドが、親ストランドと同じ OS スレッドで実行される保証はありません。 Windows® の CRITICAL_SECTION など、ほとんどのロック同期オブジェクトは、それらを取得したスレッドによって解放されなければなりません。
cilk_sync は、インテル® Cilk™ Plus のランタイム同期オブジェクトにアプリケーションを適合します。 これにより、予期しない相互作用が発生する場合があります。次のシリアルコードについて考えてみます。
次のシリアルコードについて考えてみます。
#include <cilk/cilk.h> #include <tbb/mutex.h> #include <iostream> void child (tbb::mutex &m, int &a) { m.lock(); a++; m.unlock(); } void parent(int a, int b, int c) { tbb::mutex m; try { cilk_spawn child (m, a); m.lock(); throw a; } catch (...) { m.unlock(); } }
このコードには、cilk_spawn を含む try ブロックの最後に暗黙的な cilk_sync があります。 例外が発生すると、すべての子が完了するまで実行を続行することができません。親が子よりも先にロックを取得した場合、すべての子が完了するまで catch ブロックを実行できず、一方、子はロックを取得するまで完了できないため、デッドロックが発生します。この場合、"ガード" または "スコープロック" オブジェクトを使用しても、ガード・オブジェクトのデストラクターは catch ブロックが終了するまで実行されないため意味がありません。
さらに、このコードには不可視の try ブロックがあります。 自明でない (non-trivial) デストラクターとしてローカル変数を宣言する複合文には、暗黙的な try ブロックが含まれています。そのため、プログラムはスポーンまたはロックを取得する前に、すでに try ブロックを開始している可能性があります。
関数に子によって取得されたロックがある場合、その関数はロックをリリースする前に例外をスローすべきではありません。ただし、ほとんどの関数は例外をスローしないことを保証できないため、次の規則に従います。
子ストランドが取得する可能性があるロックは取得しないでください。つまり、ロックは同じレベルのストランドに対して使用し、子ストランドに対して使用してはなりません。
子ストランドに対してロックを使用しなければならない場合は、ロックの取得、処理の実行、ロックの解放を行うためのコードを同じ関数内に記述しスポーンするのではなく、別の関数として記述し呼び出すようにします。
親ストランドがロックを取得しなければならない場合は、(構造体などで) 1 つまたは複数のプリミティブ型の値を設定してから、ロックをリリースするようにします。ロックを所有している間のスロー (演算子の多重定義を含む)、スポーン、同期を伴う関数呼び出しは、try ブロックを配置しないことで安全性を確保できます。この場合、ロックを取得する前に、プリミティブ値をあらかじめ計算しておく必要があります。