POS49-C. データが複数のスレッドからアクセスされる場合、ミューテックスを使って隣接するデータがアクセスされないよう保護する
複数のスレッドが共有変数に対するアクセスや変更を行わねばならない場合、メモリ上で隣り合う他の変数に対してそれらの操作を意図せずに行ってしまうことがある。これは、1 バイトで複数の変数を保持するなど変数をコンパクトに格納する際の弊害として発生するが、一方でワードマシン上ではよく行われる最適化である。言語仕様上、複数のビットフィールドを1バイトもしくは1ワードに格納するような最適化が許されているため、ビットフィールドは特にこの影響を受けやすい。つまり、競合状態は、1つの変数を複数のスレッドがアクセスする場合だけでなく、複数の変数が同一バイトアドレスあるいは同一ワードアドレスを共有する場合においても発生しうることを意味する。このガイドラインは、「CON32-C. 複数スレッドによる隣接データへのアクセスが必要な場合データ競合を防止する」の POSIX スレッドを使用した具体例である。
並行プログラミングにおいて競合状態を防ぐ手段は一般にミューテックスである。すべてのスレッドが適切に振舞うかぎりにおいて、ミューテックスはスレッド間で共有される変数を安全かつセキュアにアクセスするすべを提供する。しかし、共有変数へのアクセスによって他の変数へのアクセスも発生する場合、それら副次的な変数アクセスの安全性については何の保証もない。
残念ながら、ある変数と一緒にどの変数が隣り合わせに格納されているかを調べる可搬性のある方法は存在しない。
より優れたアプローチは、並行アクセスされる変数を、long
変数やパディングと一緒に共用体の中に埋め込み、そのアドレスでアクセスされるのが並行アクセスされる変数だけになるよう保証することである。この手法により、並行アクセスされる変数に対するアクセスや変更が他の変数に及ぼす影響を効果的に防止することができる。
違反コード (ビットフィールド)
次の違反コード例では、2つの実行スレッドがグローバル変数 flags
の2つのメンバに同時にアクセスしている。
struct multi_threaded_flags {
unsigned int flag1 : 2;
unsigned int flag2 : 2;
};
struct multi_threaded_flags flags;
void thread1(void) {
flags.flag1 = 1;
}
void thread2(void) {
flags.flag2 = 2;
}
コードに問題はないように見えるが、flag1
と flag2
は同じバイトに格納される可能性が高い。これら 2 つの変数に対する代入を行うスレッドが交互に実行され、値の格納が交互に行われると、一方の変数だけが意図したとおりに設定されもう一方は以前のまま、という状況が発生する可能性がある。これは、どちらのビットフィールドも同じバイト(プロセッサが操作できる最小単位)を使って表現されているためである。
たとえば、次の一連のイベントが発生する可能性がある。
Thread 1: register 0 = flags Thread 1: register 0 &= ~mask(flag1) Thread 2: register 0 = flags Thread 2: register 0 &= ~mask(flag2) Thread 1: register 0 |= 1 << shift(flag1) Thread 1: flags = register 0 Thread 2: register 0 |= 2 << shift(flag2) Thread 2: flags = register 0
各スレッドが別々のビットフィールドを変更している場合であっても、メモリ内では同じ位置を変更していることになる。これは「CON00-C. 複数のスレッドによる競合状態を避ける」で説明されている問題と同じであるが、同じメモリ位置が変更されているとは一見してわからないため、診断は難しい。
適合コード (ビットフィールド)
次の適合コードでは、ミューテックスを持つフラグへのアクセスをすべて保護し、スレッドの交錯を防いでいる。さらに、フラグを volatile
宣言することで、コンパイラがフラグに対する演算をミューテックスの外側に移動しようとしないよう保証している。最後に、フラグは共有体の中で long
の隣に埋め込まれており、静的アサートによりフラグが long
以上の領域を占有しないことを保証している。これにより、ミューテックスがチェックしていないデータがアクセスされたり変更されることを防止する。
struct multi_threaded_flags { volatile unsigned int flag1 : 2; volatile unsigned int flag2 : 2; }; union mtf_protect { struct multi_threaded_flags s; long padding; }; static_assert(sizeof(long) >= sizeof(struct multi_threaded_flags)); struct mtf_mutex { union mtf_protect u; pthread_mutex_t mutex; }; struct mtf_mutex flags; void thread1(void) { int result; if ((result = pthread_mutex_lock(&flags.mutex)) != 0) { /* エラー処理 */ } flags.u.s.flag1 = 1; if ((result = pthread_mutex_unlock(&flags.mutex)) != 0) { /* エラー処理 */ } } void thread2(void) { int result; if ((result = pthread_mutex_lock(&flags.mutex)) != 0) { /* エラー処理 */ } flags.u.s.flag2 = 2; if ((result = pthread_mutex_unlock(&flags.mutex)) != 0) { /* エラー処理 */ } }
静的アサートの詳細は「DCL03-C. 定数式の値をテストするには静的アサートを使う」を参照。
リスク評価
競合ウィンドウは小さいが、データの誤解釈が原因で代入や式の評価が正しく行われなかった場合、実行状態に悪影響を与えたり意図しない情報漏えいにつながる可能性がある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
POS49-C |
中 |
中 |
中 |
P8 |
L2 |
参考資料
[ISO/IEC 9899:2011] | Section 6.7.2.1, "Structure and Union Specifiers" |
翻訳元
これは以下のページを翻訳したものです。