👋

RustのAtomicUsize

2025/02/11に公開

AtomicUsizeとは

https://doc.rust-lang.org/std/sync/atomic/struct.AtomicUsize.html

AtomicUsizeは、複数のスレッド間で安全に共有できる整数型(usize)をラップした構造体です。
通常の変数はスレッド間で同時に読み書きするとデータ競合が起こる可能性がありますが、AtomicUsizeはアトミック操作を用いることで、こうした競合を防ぎます。

使い方

値の書き込み(store)

use std::sync::atomic::{AtomicUsize, Ordering};

let atomic_value = AtomicUsize::new(10);
atomic_value.store(100, Ordering::SeqCst);

値の読み込み(load)

use std::sync::atomic::{AtomicUsize, Ordering};

let atomic_value = AtomicUsize::new(42);
let value = atomic_value.load(Ordering::SeqCst);

値の加算(fetch_add)

use std::sync::atomic::{AtomicUsize, Ordering};

let atomic_value = AtomicUsize::new(0);
let prev = atomic_value.fetch_add(5, Ordering::SeqCst);
// 注意点: fetch_addの戻りは、加算前の値(今回でいうと0が返る)
println!("前の値: {}", prev); => 0
let value = atomic_value.load(Ordering::SeqCst);
println!("現在の値: {}", value); // => 5

メモリオーダリング(Ordering)について

各操作(load, store, swap, compare_exchange など)では、Orderingという引数を指定します。

https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html

これは、メモリの読み書きの順序に関するルールを定めるもので、以下のような種類があります。

メモリオーダリング 説明 主な用途・特徴
Relaxed アトミック性は保証するが、他のメモリ操作との順序は保証しない。 順序性が問題とならない単純な更新(例:カウンターの加算)に適しており、パフォーマンス重視の場合に使用。
Acquire 読み込み操作用。対象の値を読み込んだ後、その値に依存する処理が他のスレッドで行われた更新を確実に反映する。 他スレッドからのデータ公開(store側にReleaseが用いられる)を読み込む際に使用。
Release 書き込み操作用。それまでのメモリアクセスが完了していることを保証し、他スレッドが読み込む際にその順序を維持する。 データの公開時(例えば、ロック解除とともに値を更新する場合)に使用。
AcqRel 読み込みと書き込みの両方の特性を併せ持つ(Read-Modify-Write操作用)。 CAS(Compare And Swap)やswapなど、値の読み取りと同時に更新が必要な操作に適用。
SeqCst 全てのスレッドで同じ順序で操作が観測される、最も厳格な順序保証。 順序の整合性が最重要な場合に使用。ただし、最も厳格なためパフォーマンスのコストが高くなる可能性がある。

パフォーマンスにそれほど懸念がなく、ともかく更新順序も厳格に保証したい場合は、SeqCstを使っておけば良さそうです。

サンプルコード


use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

fn main() {
    // AtomicUsizeのインスタンスを生成(初期値0)
    static COUNTER: AtomicUsize = AtomicUsize::new(0);

    // 10スレッドを生成して、counterを更新する
    let handles: Vec<_> = (0..10)
        .map(|_| {
            // counterへの参照をクローン(コピー)して各スレッドで利用
            thread::spawn(move || {
                for i in 1..=100 {
                    // fetch_addで1~100までの値を加算 -> 5050
                    COUNTER.fetch_add(i, Ordering::SeqCst);
                }
            })
        })
        .collect();

    // 全てのスレッドが終了するのを待つ
    for handle in handles {
        handle.join().unwrap();
    }

    // 最終的なカウンターの値を取得
    let result = COUNTER.load(Ordering::SeqCst);
    println!("最終的なカウンターの値: {}", result); // 5050 * 10 => 50500
}

Discussion