はじめに
この記事は、インドリ氏による『スレッドセーフとインテルTBBのコンテナ』に記載されている誤りを訂正することを目的としています。インドリ氏の記事では、TBBコンテナの紹介に注意するあまり、マルチスレッドプログラミングに潜む危険、その危険を取り除く方法についての記述が正しくありません。本記事では、マルチスレッドプログラミングを安全に設計する方法を説明することを目的とします。
本記事で用いるコードは、C言語に類似していますが、C言語ではありません。振る舞いを理解していただきやすくするための仮想言語です。実行できる環境はありません。
「競合」という問題
ここに、1つのリンゴがあります。そして、2人の人が、そのリンゴの前にいます。ここで2人に向かって「リンゴを食べて良いですよ」とだけ言うと、どうなるでしょうか。お互いに譲り合うか、もしくは取り合いをするでしょう。ここで2人が仲良くリンゴにありつくためには、調停者がいて、どのように分けるかを決めることが必要です。もちろん、2人のうちどちらかが調停者を兼ねてもかまいません。
並列プログラミングを行うということは、このように、2人以上の人に何らかの作業を依頼することにたとえられます。これらの作業を依頼された人がそれぞれ別個の作業を行うなら、何の問題もありません。しかし、複数の人が同じ資源を操作するようなことがあると、問題が発生します。
List1は、1000個の文字を格納できるスタックです。
#define ARRAY_MAX 1000 static char array[ARRAY_MAX]; static int index = 0; int push(char v) { if (index < ARRAY_MAX) { array[index] = v; ++index; } return index; } char pop(void) { if (index > 0) { return array[index]; --index; } return NULL; } int howmany(void) { return index; }
このコードは、シングルスレッドで実行している限り、安全に実行できます。しかし、マルチスレッドでは、安全ではありません。マルチスレッドでは、コードが同時に実行されます。次のように実行がなされたとき、どのようになるでしょうか。なお、これらの実行直前で、index
は999、つまりあと1つなら追加できる状態で、2つ追加しようとしている状況です。
実行前の | スレッド1 | スレッド2 | 実行後の |
index値 | 実行行 | 実行行 | index値 |
999 | int push(char v) { |
999 | |
999 | if (index < ARRAY_MAX) { |
999 | |
999 | array[index] = v; |
int push(char v) { |
999 |
999 | ++index; |
if (index < ARRAY_MAX) { |
1000 |
1000 | return index; |
array[index] = v; |
1000 |
1000 | |
++index; |
1001 |
1001 | |
return index; |
1001 |
スレッド2のif
文でindex
値を参照した後、スレッド1のインクリメントが実行され、index
の値が1000となりました。するとスレッド2では、array
配列の1001番目の要素にアクセスすることになります。これを実行した結果どうなるかは、実行環境により異なりますが、アプリケーションのどこかで、期待しないエラーが出る可能性が高くなります。
さて、このコードの問題は何でしょうか。それは、index
変数の1つのインスタンスが複数のスレッドで共有されることです。index
変数がどのようなアクセススコープを持っていても、関係はありません。複数のスレッドが同じインスタンスを共有すると、問題が発生します。
逐次処理で発生する並列化と同じ問題
(「並列処理とコンテナ:スレッドセーフとは何か」より)
並列プログラミングを行う際には、従来の逐次プログラミングでは考えなかったことを考えなくてはなりません。そのうちの1つが、並列処理で同時にコンテナを操作する時に起こる問題についてです。これはよく「マルチスレッドプログラミングではスレッドセーフなコンテナを使う必要がある」と表現されます。
インドリ氏のこの書き方は、誤解を招く表現を多く含んでいます。
1)「並列プログラミング」に対する誤解を与える
「並列プログラミングを行う際には」という表記と、「マルチスレッドプログラミングでは」という表記が混在することにより、並列プログラミングをマルチスレッドプログラミングに限定するような表現がされています。しかし、「並列プログラミング」には「マルチスレッド」以外にも「マルチタスク」があります。もっとも、「マルチタスク」の場合は「プログラミング」という小さな単位ではなく、システムという大きな単位になります。
2)従来の逐次プログラミングというほど、並列プログラミングは新しいわけではない
「並列プログラミング」には、「マルチスレッド」だけでなく「マルチタスク」が含まれます。マルチタスクはMS-DOSからWindowsに移行したとき、すなわちWindows2.0から発生しており、これは1987年に誕生しています。ただし、完全なマルチタスクOSには1993年のWindows NT 3.1まで待たなければなりません。しかし、UNIXについては、生まれたときから完全なマルチタスクOSでした。
3)並列プログラミングに限った問題ではない
スレッドクリティカルの根本的な問題は、複数の処理が同じインスタンスを操作することです。これは、並列プログラミングに限った問題ではありません。データベースを扱う場合や、非同期実行を行う場合、一時ファイルやセマフォといった共有メモリなどを扱う場合など、逐次プログラミングであっても同様に発生します。