Top ›
C++ 入門 › STEP 12 › 第70回 std::atomic の基礎
第70回 std::atomic の基礎 — ロック無しで安全な共有
前回の mutex は「複数行の作業」を守るのに向くが、1 変数だけ を守るにはオーバーヘッドが大きい。std::atomic<T> は CPU の原子命令を使い、ロック無しで安全に ++ や読み書きを行えます。
最低限ここだけ
std::atomic<int> cnt{0};
cnt++/cnt.load()/cnt.store(x)
1 変数だけなら mutex より速い
余裕があれば
fetch_add / compare_exchange
memory_order の 3 レベル
atomic_flag と spinlock
このページの流れ
1. まず触ってみる — atomic カウンタ
2. mutex との性能比較
3. 主要メンバ関数
4. compare_exchange と CAS
5. memory_order をざっくり
6. 実務の注意
7. 理解度チェック
1. まず触ってみる — atomic カウンタ
第 59 回の int n; を std::atomic<int> n; に変えるだけで、データ競合が消えます。CPU のハードウェア命令が「読んで+1して書く」を不可分に 実行してくれる、というのが正体です。
1-1. イメージ — 回転扉
mutex は「鍵のかかる個室(複数行の作業を全部守れる)」。atomic は「1 回転 1 人の回転扉 (1 つの値の更新だけ、しかし一瞬)」。単一変数しか守らない代わりに、鍵の掛け外しが不要で桁違いに速い。
1-2. 最小コード
atomic_basic.cpp C++
#include <atomic>
#include <thread>
std::atomic<int > n{0 }; // ← 0 で初期化
void inc () {
for (int i=0 ; i<100000 ; ++i)
++n; // ← 原子的にインクリメント
}
int main () {
std::thread a(inc), b(inc);
a.join (); b.join ();
std::cout << n; // 必ず 200000
}
++n は内部で fetch_add(1) に展開され、x86 では lock xadd 命令になります(1 命令)。mutex の lock/unlock(複数命令 + カーネル呼び出しの可能性)より圧倒的に軽量です。
1-3. よくある質問
Q. 何でも atomic にできる? 自明に複製できる型 (POD / trivially copyable)なら OK。std::string は不可。基本型・ポインタ・small struct が主な対象です。
Q. 真にロックフリー? ほとんどの実装で int / long / ポインタはハードウェア命令 で実装されロックフリー。16 バイトを超える型はロック実装にフォールバック。is_lock_free() で確認可能。
Q. volatile と何が違う? volatile はコンパイラ最適化抑制 (デバイス I/O 向け)。マルチスレッド同期には使えない 。必ず atomic を使う。
2. mutex との性能比較
同じ「2 スレッドで 100 万回 ++」を mutex 版と atomic 版で比較するとだいたいこうなります(環境により差はあります)。
▶ 実行時間の目安
ベンチ実行
リセット
std::mutex + lock_guard で ++counter
計測結果がここに表示されます。
ただし万能ではない : 複数の変数をまとめて更新したい(例: 送金先から減らして送金元に足す)場合、atomic だけでは不可能。そういう時は mutex が必要。
3. 主要メンバ関数
操作 意味 例
load()現在の値を読む int v = n.load();
store(x)値を書く n.store(42);
exchange(x)書いて旧値を返す int old = n.exchange(0);
fetch_add(n)加算して旧値を返す int idx = n.fetch_add(1);
fetch_sub(n)減算
fetch_or/and/xorビット演算 フラグビットの更新
compare_exchange_weakCAS(後述) ロックフリー DS
3-1. 演算子と fetch_xxx は等価?
ops.cpp C++
std::atomic<int > n{0 };
++n; // fetch_add(1) と等価
n++; // 同上(旧値を返す点も同じ)
n += 5 ; // fetch_add(5)
n = 10 ; // store(10)
int v = n; // load()
// ただし "n = n + 1" は原子ではない(load → add → store の 3 段階)
n = n + 1 ; // ← NG: 途中で割り込まれうる
4. compare_exchange と CAS
CAS(Compare-And-Swap)は「現在の値が期待通りなら新しい値に置き換える 」原子操作。ロックフリーデータ構造を組む基本パーツ です。
cas.cpp C++
std::atomic<int > n{0 };
int expected = 0 ;
bool ok = n.compare_exchange_strong (expected, 42 );
// もし n == 0 なら n = 42 にして ok = true
// そうでなければ expected に現在値を書き戻して ok = false
4-1. CAS ループで「原子な更新」を合成
cas_max.cpp C++
// 最大値への atomic update(ライブラリに無いので自作)
void atomic_max (std::atomic<int >& a, int v) {
int old = a.load ();
while (v > old &&
!a.compare_exchange_weak (old, v)) {
// 失敗時 old に最新値が入ってループ再試行
}
}
// ロックを使わず複雑な更新を合成できる = lock-free パターン
weak と strong の違い : weak は偽失敗 (値が一致してても失敗する)を許容する代わりに速い。ループの中では weak、単発なら strong。
5. memory_order をざっくり
atomic 操作にはメモリ順序 という引数があります。CPU は速度のため命令を並べ替える(out-of-order execution)ことがあり、スレッド間で見える順序 をどこまで保証するかを指定します。
memory_order 意味 使いどころ
seq_cst(既定)最強保証。全スレッドが同じ順序で見る 迷ったらこれ
acquire / releaseペアで使う。リリース後の書き込みがアクワイア以降に見える フラグ同期
relaxed原子性は保証、順序は気にしない 単純カウンタ(統計値など)
relaxed.cpp C++
std::atomic<long > hits{0 };
void onReq () {
// 統計カウンタだけ。他の変数との順序は不問
hits.fetch_add (1 , std::memory_order_relaxed);
}
acq_rel.cpp C++
std::atomic<bool > ready{false };
int data;
// プロデューサ
data = compute ();
ready.store (true , std::memory_order_release);
// コンシューマ
while (!ready.load (std::memory_order_acquire)) {}
use (data); // ← data の書き込みが見える
結論 : 性能にシビアでない限り既定の seq_cst で書く 。最適化したい箇所だけ緩める。memory_order の誤用は再現性の低いバグに直結するので、緩めるなら Thread Sanitizer で検証 しましょう。
6. 実務の注意
① 1 変数の単純更新なら atomic、複数変数や条件付き更新なら mutex 。この判断軸で普段は十分。
② atomic 同士の組み合わせは原子にならない 。a.load(); b.store(a.load()+1); は 3 つの原子操作で、間に割り込まれる。
③ false sharing に注意 。別々の atomic が同じキャッシュラインに乗ると互いを遅らせる。alignas(64) で分離。
④ atomic<shared_ptr> (C++20)も用意されている。スレッドセーフな所有権更新に。
⑤ ロックフリーは難しい 。自作 lock-free キューは専門家の仕事。普段は concurrent_queue ライブラリを使おう。
STEP 12 のまとめ : 並行処理は「thread で起動 → mutex/atomic で共有データ保護 → async/future で結果受け取り」の 3 点セット。さらに jthread(C++20)や coroutine(C++20)で書き味は改善中。この基礎を押さえれば現実のマルチコア性能を引き出せます。
7. 理解度チェック
4 問。
Q1. 次のうち atomic で安全にできる操作は?
std::atomic<int> n; n.fetch_add(1);
std::atomic<int> n; n = n + 1;
std::atomic<std::string> s;(中身を書き換える操作)
fetch_add は 1 命令で原子。n = n + 1 は load→add→store の 3 ステップで途中割り込まれうる。string は trivially copyable ではないので atomic に入れられない。
Q2. 単純なカウンタを守るのに最適なのは?
std::mutex + lock_guard
std::atomic<int>
volatile int
1 変数なら atomic が最速。mutex も動くが重い。volatile は最適化抑制のみで同期には使えない。
Q3. compare_exchange_weak が偽失敗することがあるのはなぜ?
一部 CPU の LL/SC 命令が内部的にスプリアスフェイルするため、許容してループで再試行させる方が全体として速い
C++ 標準のバグ
実装手抜きのため
ARM などの LL/SC は割り込みやキャッシュラインの追い出しで失敗しうる。strong はそれを包み隠すループを内包するため微妙に重い。CAS ループの中では weak が標準。
Q4. memory_order の既定値(seq_cst)の特徴は?
最も緩い保証で最も高速
最も強い保証。全スレッドが同じ順序で観測でき、迷ったらこれを使えばよい
指定しないとコンパイルエラーになる
seq_cst は全順序を保証する最強モード。代わりに最も遅い。性能問題がない限りこれで書くのがモダン C++ のベストプラクティス。