「競合問題」の解決
「1つのインスタンスが複数のスレッドで共有される」ことで問題が発生するなら、スレッドごとにインスタンスを分ければ、問題は解決します。最初からマルチスレッド化することがわかっていて、設計から行うならば、設計の段階でマルチスレッド化する箇所を洗い出し、スレッド間で共通して使用するインスタンスがないように設計を行います。
しかし、今回取り上げたような、オブジェクトを保存しておくためのアルゴリズム、「コンテナ」では、データを蓄えている変数や、どこまでデータを蓄えているかを示すインデックスなどのインスタンスを、複数のスレッドで共有しなければ意味をなしません。そこで、共有しながら解決できる方法を探します。
では、もう一度問題に戻ります。先ほど「複数のスレッドで、インスタンスを共有することが問題」と説明しました。「インスタンスを共有すること」が、なぜ問題になるのでしょうか。共有しなければならないなら、共有することで起こる問題を解決する方法を探そう、ということです。すなわち「共有しても問題のないアルゴリズム」が、解決方法となるわけです。
前ページで記載した表1を見ます。スレッド2がindex
の範囲を確認した後に、スレッド1でindex
の値を変更しています。こうすると、せっかく範囲を確認したことが無駄になってしまいます。つまりここは、index
の範囲を確認してからインクリメントするまでの間、他のスレッドがindex
の値を参照してはいけないのです。そこで、この範囲を「複数のスレッドで同時に実行すること」を禁止します。
禁止する方法は、処理系によって用意されています。ここでは「lock (オブジェクト) 処理
」とすることで、同じ「オブジェクト
」を参照するスレッドからは「処理
」が実行できない、という実装であると仮定し、List1を修正します。Windowsでは、ミューテックスを使ってロックを行う場合が多いです。OpenMPでは、クリティカルディレクティブによって指定します。使用している処理系による禁止の方法を調べて使用してください。
#define ARRAY_MAX 1000 static char array[ARRAY_MAX]; static int index = 0; static int lockObject = 0; int push(char v) { lock (&lockObject) { if (index < ARRAY_MAX) { // 参照して、 array[index] = v; ++index; // 変更する } } return index; } char pop(void) { lock (&lockObject) { if (index > 0) { // 参照して、 return array[index]; --index; // 変更する } } return NULL; } int howmany(void) { lock (&lockObject) { return index; } }
実行前の | スレッド1 | スレッド2 | 実行後の |
index値 | 実行行 | 実行行 | index値 |
999 | int push(char v) { |
999 | |
999 | lock (&lockObject) { |
int push(char v) { |
999 |
999 | if (index < ARRAY_MAX) { |
lock (&lockObject) { |
999 |
999 | array[index] = v; |
ロック待ち | 999 |
999 | ++index = v; |
ロック待ち | 1000 |
1000 | } // lock 終了 |
if (index < ARRAY_MAX) { |
1000 |
1000 | return index; |
} // lock 終了 |
1000 |
1000 | |
return index; |
1000 |
「1行」なら安全か
(コメント インドリ (2010/03/03 09:12)より)
>スケジューラーの初期化について
並列処理では実行順序が保障されません。しかも、アセンブラレベルで命令が実行されております。その状態なのですから、初心者がTBBとネイティブスレッド・OpenMPなどの処理を併用させたときに、どのようなプログラムでも正しく実行できると100%の自信を持って言えるでしょうか?私はそんな危険を冒すよりも1行のプログラムを書く方を勧めます。
(コメント インドリ (2010/03/03 15:55)より)
ですから普通の人は・・・
cout << "foo" << .....
(なんらかの処理をする)
cout << "bar" << ...
とプログラミングするでしょう。
また必ず一行で書くという行為は
cout
という標準出力オブジェクトの不変項と事後条件を満たしていると言えるでしょうか?
本文中に、「cout
はスレッドセーフではない」と書かれていることについてです。ここに引用したように、「一行で書けば安全だ」と理解できる論調なので、これについて間違っていることを解説します。
まず、「cout
はスレッドクリティカルか」という問題についてです。私が調べた範囲、Microsoft、SUNによる実装は、スレッドセーフであるとリファレンスに記載されています。GNUについては、POSIXがスレッドセーフであることを要求するので、準拠したライブラリを使用するならばスレッドセーフであると、記載されています。参考資料に挙げておきます。よほどユニークなライブラリでない限り、「スレッドセーフである」と言って大丈夫でしょう。ただし、注意してください。C++の標準では、スレッドセーフについて規定されていません。お使いの環境で、ドキュメントを確認してから使用してください。
次に、「一行で書く」ということについてです。本記事では、「変数を参照する」「変数を更新する」という“2行”に渡る処理を取り上げました。これが“1行”だったら問題が発生しないかと言うと、そうではありません。インドリ氏もコメントでは触れられていますが、人が書く1行のコードは、複数のアセンブリコードに翻訳(コンパイル)されます。アセンブリコードが複数の命令になる場合、問題が発生する可能性があります。アセンブリコードが想像できなくても大丈夫です。for
文を思い浮かべてください。標準的なfor
文は、1行の中に3つの実行文があります。同じように、List3のように書けば、複数行でありながら1実行文となります。ソースコードの行数と実行される命令数、アセンブリレベルでの命令数には、何の関係もありませんし、関係を持たせるべきではありません。
int c = 1;
最後に、もっとも重要なことです。インドリ氏の一連の発言では「関数がスレッドセーフであること」と、「アプリケーションがスレッドセーフであること」を混同しています。この混同のために、コメント投稿者との会話に齟齬が発生しています。インドリ氏以外は、この2つを混同していません。この2つは同じではなく、分けて考えなければなりません。例えば、インドリ氏が「並列処理で使用して安全である」として取り上げている「concurrent_bounded_queue
」であっても、複数のスレッドから同時にpush
した場合、どの順番でコンテナに挿入されるかは分かりません。これは、cout
に対して「並列処理時にcout
に出力を指定するとコンソール画面の表示が滅茶苦茶になります」と書いてあるのと同じことなので、concurrent_bounded_queue
も「スレッドセーフではない」事になってしまいます。インドリ氏の記事では、他の記事を見ても、1命令を多数のスレッドで同時に呼び出すということをしていないため、この問題に気がつかなかったと思われます。
繰り返しますが、スレッドセーフな命令だけで作ったからといって、アプリケーションが「スレッドセーフ」、この場合は設計意図通りに実行結果が現れるわけではありません。アプリケーションがスレッドセーフになるためには、開発者がスレッドクリティカルな部分について適切に処理を組む必要があります。
上の引用について、私のブログの方で「「1行のプログラムを書く」というのは、TBBの初期化を指しているのではないか。」というご指摘をいただきました。なるほど、「>スケジューラーの初期化について」とされているので、「task_scheduler_init init;
」の一行を挿入することと理解する方が正しい様です。ただ、私がここに「挿入する」と書いたように、「1行のプログラムを書く」ではなく、「1行挿入すること」と書いてあれば、より誤解が少なかったでしょう。