🐸

なんでもatomicにするな

2022/07/08に公開
4

0. はじめに

マルチスレッドプログラムで、作業メモリをなんでもatomic変数にする人がいる。

良く言えば用心深い人。
ただ、マルチスレッド化の(大抵の)目的は高速化なのに、過剰な排他制御で本末転倒になる場合がある。

この記事では、排他制御が不要な場合と、必要な場合を作業メモリの観点で示す。

説明を単純化するため、スレッドが2つの場合を示す。

更新

  • 2022.11.10: 排他制御が不要な場合1.3を、排他制御が必要に修正

1. 排他制御が不要な場合

1.1 作業メモリが違う

1.2 作業メモリが同じだが、読込だけ

1.3 作業メモリが同じだが、書込みするのは1スレッドだけ

2. 排他制御が必要な場合

2.1 作業メモリが同じで、2スレッド以上から書込む

2.2 作業メモリが同じで、2スレッド以上から読み書込き

2.3 作業メモリが同じだが、書込みするのは1スレッドだけ

3. まとめ

  • 同じ作業メモリに書込みをするスレッドが2スレッド以上あれば排他制御が必要と覚える
  • 同じ作業メモリを読み書込みをするスレッドが2スレッド以上あれば排他制御が必要と覚える
  • 例外はあるかもしれん(用心深い人は私です)

Discussion

lempijilempiji

図がわかりやすくて良いですね。
ただ1.3は書き込み操作が含まれるため排他制御が必要ではないでしょうか?
データ競合という問題で、C言語の例としては以下のページに記載があります。多くの言語で同じ問題が起きると思います。
https://www.jpcert.or.jp/m/sc-rules/c-con32-c.html

s9s9

コメントありがとうございます。
提示いただいたリンク先には2つの例があり、この記事で触れてない1.1の例外という意味で面白い題材です。

ちょっと深堀してみます。

リンク先の例①

2つのスレッドから異なる作業メモリを変更する(書込みする)ので、1.1の例外に該当しますね。

// https://www.jpcert.or.jp/m/sc-rules/c-con32-c.htmlより引用
struct multi_threaded_flags {
  unsigned char flag1;
  unsigned char flag2;
};
struct multi_threaded_flags flags;
void thread1(void) {
  flags.flag1 = 1; // 書込み
}
void thread2(void) {
  flags.flag2 = 2; // 書込み
}

リンク先の解説にあるように

  • "C99"では競合する
  • "C11"では競合しない

なので、C99では排他制御が必要です。図で書くと以下になると思います。

リンク先の例②

こちらも1.1の例外ですね。

// https://www.jpcert.or.jp/m/sc-rules/c-con32-c.htmlより引用
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; // 書込み
}

リンク先の解説にあるように

  • "C99"では競合する
  • "C11"でも競合する

なので、排他制御が必要です。図は例①とほぼ同じすね。
違いは以下なので、図は省略します

  • 例①では、flag1, flag2の所要ビット数がそれぞれ8bit
  • 例②では、flag1, flag2の所要ビット数がそれぞれ2bit

まとめ

この2例をまとめると、”異なる作業メモリに2スレッド以上で書込む場合、作業メモリが2バイト以下に隣接してると、競合する場合がある”
ということでしょう。

面白い話題のご提供ありがとうございました。

s9s9

参考記事つきで、コメントありがとうございます。

ご指摘の通りです。

C++ではatomic変数にしないと、
異なるスレッドからの値が読めるようになるという意味の可視性を保証する術がなさそうなので
同期は必要ですね。

修正しておきます。