覚書 引用 ChatGPT
こちらこそ、いつも本質的で良い質問ありがとうございます!
スレッドと static/ローカル変数の関係について、以下に分かりやすくまとめます。
結論(先に要点)
種類 各スレッドで独立? 備考
関数内ローカル変数 独立している スタック領域に生成、スレッドごとに別物
static 変数(関数内) ❌ 共通(共有される) グローバル領域に一つだけ存在する
クラスの static 変数 ❌ 共通(共有される) 全インスタンス&全スレッドで共通
非 static メンバ変数 ✅ インスタンスごとに独立 ただし同じインスタンスを共有したら当然共有される
🧪 詳しく見てみましょう
🧷 1. ローカル変数(関数内で宣言された変数)
static void fcPublic() {
int localVar = 42; // これは各スレッド独立
}
これは スレッドごとのスタック領域に確保されます。
よって、スレッド間で完全に独立します。
✔️ 安心して使えます。マルチスレッド安全です。
🧷 2. static ローカル変数(関数の中で static)
static void fcPublic() {
static int sharedCounter = 0; // これは全スレッドで共通
}
一度だけ初期化されて共有される。
複数スレッドから同時アクセスした場合は競合が起きる可能性あり。
→ ミューテックスなどで保護すべきです。
⚠️ マルチスレッドでは注意が必要です。
🧷 3. クラスの static メンバ変数
class MyClass {
static int gCounter; // スレッド共通
};
全てのスレッド・全てのインスタンスで 共通の一個しか存在しない。
これもスレッドセーフにするにはロックが必要です。
⚠️ 使うなら std::mutex 等で排他制御が必要です。
🧷 4. 非 static メンバ変数
class MyClass {
int iData; // インスタンスごとに違う
};
インスタンスをスレッドごとに分ければ問題なし。
ただし、同じインスタンスを複数スレッドが共有すると競合の可能性あり。
✔️/⚠️ 安全かどうかは設計次第です。
🧵 例:std::thread で static メンバ関数を使う
class MyClass {
public:
static void run() {
int localVar = 100; // スレッドごとに独立
static int staticVar = 0; // スレッド間で共有、危険
++staticVar;
std::cout << "localVar: " << localVar << ", staticVar: " << staticVar << "\n";
}
};
std::thread th1(MyClass::run);
std::thread th2(MyClass::run);
th1.join();
th2.join();
localVar → 問題なし、毎回 100
staticVar → 競合の可能性。値が 1,2 になる保証はない。データレース。
🧩 対処法:static 変数のスレッドセーフ化
方法1: ミューテックスで保護
static std::mutex mtx;
static int sharedCounter = 0;
std::lock_guard<std::mutex> lock(mtx);
++sharedCounter;
方法2: スレッドローカル変数(thread_local)
static thread_local int threadCounter = 0;
→ スレッドごとに独立な static 変数。
まさに「スレッドごとの static」が欲しいときに便利。
✅ まとめ(再掲)
変数の種類 スレッド間で共有? 安全に使える?
関数内ローカル変数 ❌ 共有されない ✅ 安全
static ローカル変数 ✅ 共有される ⚠️ 要注意
クラスの static メンバ変数 ✅ 共有される ⚠️ 要注意
非 static メンバ変数 ❌ 共有されない(※) ✅ or ⚠️(設計次第)
thread_local 変数 ❌ スレッドごと ✅ 安全
何か試してみたいコードがあれば、書いて一緒に確認もできますよ!
あなた:
ありがとうございます。
質問があります。
ただし同じインスタンスを共有したら当然共有される
その時、ほぼ同時には代入されないと思いますが、
共有を許容した場合、特別な処理なく、同時に代入しても大丈夫ですか?
ChatGPT:
とても重要な視点です。
結論から言うと:
✅ 結論:同時に代入しては「ダメ」です。
同じインスタンスのメンバ変数に対して
複数スレッドが同時に書き込む(代入する)と未定義動作になります。
これは「競合状態(data race)」と呼ばれ、非常に危険です。
🔥 具体例:代入がかち合うとどうなるか
class MyClass {
public:
int counter = 0;
};
MyClass obj;
std::thread t1([&]() {
for (int i = 0; i < 1000000; ++i) obj.counter = 1;
});
std::thread t2([&]() {
for (int i = 0; i < 1000000; ++i) obj.counter = 2;
});
t1.join();
t2.join();
std::cout << "Final value: " << obj.counter << "\n";
obj.counter に 1 と 2 を交互に高速で書き込む
処理系・タイミングにより:
1 や 2 のどちらかに収束するかもしれないし
破壊された中途半端な値になることすらあります(UB)
マシンによってはクラッシュや謎挙動も
📛 どうして「代入だけ」でも危険なの?
obj.counter = 1;
これ1行でも、実は複数ステップに分解されてます:
アドレス計算
レジスタへ読み出し
レジスタに値セット
メモリ書き戻し
別スレッドも同じことをしてたら?
→ 読み出しと書き戻しが競合して、予測不能な値になります
🛡️ 対処法
1. std::mutex で保護する
std::mutex mtx;
std::thread t1([&]() {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
obj.counter = 1;
}
});
→ 同時アクセスしないように排他制御。
2. std::atomic を使う(単純な整数などに限る)
#include <atomic>
class MyClass {
public:
std::atomic<int> counter = 0;
};
ロック不要でスレッドセーフにアクセス可能。
+=, ++, store, load などが使える。
高速(CPUの命令レベルで排他制御)
✅ まとめ
条件 安全性 対処方法
同一インスタンスに同時読み込み ✅ 安全 問題なし
同一インスタンスに同時書き込み ❌ 危険(UB) mutex または atomic
💡 「代入だけだし安全でしょ」は C++ では通用しないです。
処理系依存・CPU依存の落とし穴があります。