マルチスレッド処理で使う、参照カウント式のオブジェクト管理の管理プログラムの例
# include <atomic>
class RefObj
{
std::atomic<int> count;
// ...
void Unref_ng();
void Unref_ok();
void destruct();
// ...
};
void RefObj::Unref_ok()
{
if (0 == --count)
destruct();
}
void RefObj::Unref_ng()
{
--count;
if (0 == count)
destruct();
}
RefObj::Unref_ng() が駄目な理由は
// Thread A と B でほぼ同時に呼ばれる
void RefObj::Unref_ng()
{
// Thread A では 2 から 1 に減る
// Thread B では 1 から 0 に減る
--count;
// Thread A からすると「※何もない部分」で Thread B が更新した
// 結果、Thread A も B も同時に count=0 に見える可能性がある
if (0 == count)
{
// Thread A と B が同時に count=0 だと
// 同じ処理が、別々のスレッドで実行される
destruct();
// メモリ内容の破壊 = 変数が意図しないところで変更される
// 恐れあり -> 意図しない動作やクラッシュが他所で発生
}
}
が発生しうるから。
アセンブラ レベルでの動作
g++ の "-S -O3" オプションで出力されたアセンブラ(不要な情報を排除して、シンボルを修正)を見ると
RefObj::Unref_ok():
pushq %rbp
movq %rsp, %rbp
lock
decl (%rdi) ;# count をデクリメント
jne LBB1_1 ;# デクリメント結果で分岐
popq %rbp
jmp RefObj::destruct()
LBB1_1:
popq %rbp
retq
RefObj::Unref_ng():
pushq %rbp
movq %rsp, %rbp
lock ;# 直後の decl のみメモリアクセスを排他
decl (%rdi) ;# count をデクリメントして "2" から "1" になっていても
movl (%rdi), %eax ;# count を参照した時点では "0" になっているかも
testl %eax, %eax ;# count 内容で判断して
je LBB0_2 ;# 分岐
popq %rbp
retq
LBB0_2:
popq %rbp
jmp RefObj::destruct()
となってます。問題点は
lock ;# 直後の decl のみメモリアクセスを排他
decl (%rdi) ;# count をデクリメントして "2" から "1" になっていても
;# ここで、別スレッドにて count を変更することがある(※何もない部分)
;# メモリアクセスの排他によって、Thread A と B は動作タイミングが変わる
movl (%rdi), %eax ;# count を参照した時点では "0" になっているかも
で、ここでも「何もない部分」が問題になります。
8/16bit 時代のアセンブラ プログラマ間では「割り込み先で書き換わってない?」の一言で理解してもらえるのだが...
この手の不具合が厄介なのは、不具合の発生が CPU の動作タイミングに依存することで、デバッガ上では発生しない状況になりうることです。そうなると、ソース コードと睨めっこして「バグってなさそうでバグってる(概ね正常動作、時々不具合)」部分を探すしかありません。
要は「アテが外れる可能性があるコード」を重点的に探します。
操作は同じなのに、動作が不安定なソフトに遭遇する度に、この手の不具合が考えられる。担当プログラマには動作環境の所為にせず、頑張ってデバッグして欲しい...