3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust 1.98でVec<u32>をAtomicとして一時的に借りる

3
Posted at

Rust 1.98でVec<u32>をAtomicとして一時的に借りる

大量の値を普段は u32 として処理し、一部分だけ複数スレッドからatomicに更新したい。このためだけに、最初から Vec<AtomicU32> を持つ必要はありません。

Rust 1.98.0では AtomicU32::from_mut_slice がstableになり、&mut [u32] を必要な期間だけ &mut [AtomicU32] として借りられます。ポイントは型変換そのものではなく、通常アクセスとatomicアクセスを排他的borrowで同時に存在させないことです。

Atomicをデータ型にする必要はあるのか

たとえば、集計途中のカウンタを次のように持っているとします。

let mut counts: Vec<u32> = vec![10, 20, 30, 40];

大部分の処理が単一スレッドなら、これは自然です。初期化、シリアライズ、通常のループ処理などで必要なのは u32 であって、atomic operationではありません。

一方、ある短い区間だけ複数スレッドから更新するなら、以前はデータ自体を次のように設計したくなる場面がありました。

use std::sync::atomic::AtomicU32;

let counts: Vec<AtomicU32> = vec![
    AtomicU32::new(10),
    AtomicU32::new(20),
    AtomicU32::new(30),
    AtomicU32::new(40),
];

もちろん、値が共有状態として長期間存在し、常にatomic operationを要求するならこちらが素直です。

ただし「データの性質は u32 だが、特定の処理区間だけatomic operationが必要」という場合は少し意味が違います。同期の都合を、データを保持している全期間の型へ押し込めることになります。

Rust 1.98.0は2026年8月20日にリリースされ、Atomic<T>::from_mut、Atomic<T>::get_mut_slice、Atomic<T>::from_mut_slice がstableになりました。

AtomicU32 なら、次の形が使えます。

pub fn from_mut_slice(v: &mut [u32]) -> &mut [AtomicU32]

ただし、このAPIは対応するatomic型とプリミティブ型のalignmentが同じターゲットで利用できます。AtomicU32 では target_has_atomic_primitive_alignment="32" が条件です。

安全なのはAtomicだからではなく&mutだから

Rust 1.98でVec<u32>をAtomicとして一時的に借りる

ここが一番おもしろいところです。

u32 が置かれていたメモリを途中からatomic operationの対象にしてよい、とだけ聞くと危険に見えます。別スレッドが通常の u32 として読み書きしている最中に、同じ場所へatomic accessを始める設計はできません。

from_mut_slice が受け取るのは共有参照の

&[u32]

ではなく、

&mut [u32]

です。

この違いが境界になります。

&mut [u32] を借りられている時点で、その領域への競合する別の参照を同時に使うことはRustのborrow ruleによって制限されています。その排他的borrowから &mut [AtomicU32] を作るので、atomic viewが生きている期間に元のsliceへ通常アクセスすることも許されません。

つまり、

通常アクセス
    ↓
&mut [u32]
    ↓
from_mut_slice
    ↓
atomic access
    ↓
borrow終了
    ↓
通常アクセス

という時間方向の境界を型で作っています。

「同じメモリを二種類の方法で同時に触れるAPI」ではなく、「排他的に確保したメモリのアクセス方法を、そのborrowの期間だけ切り替えるAPI」です。

逆方向の AtomicU32::get_mut_slice が安全なのも同じ理由です。公式ドキュメントも、mutable referenceによって他スレッドからatomic dataへ同時アクセスされないことが保証されるため、non-atomic accessが安全だと説明しています。

スコープだけAtomicとして借りる

Rust 1.98.0で動かす最小例です。外部crateは使いません。

Cargo.toml はこれだけで構いません。

[package]
name = "atomic-view"
version = "0.1.0"
edition = "2024"

[dependencies]

src/main.rs を次のようにします。

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

fn main() {
    let mut counts = vec![10_u32, 20, 30, 40];

    // ここまでは普通の Vec<u32>
    for value in &mut counts {
        *value += 1;
    }

    {
        // このスコープだけ atomic view にする
        let atomic_counts = AtomicU32::from_mut_slice(&mut counts);

        std::thread::scope(|scope| {
            for counter in atomic_counts.iter() {
                scope.spawn(move || {
                    counter.fetch_add(100, Ordering::Relaxed);
                });
            }
        });
    }

    // atomic view の borrow が終わったので、
    // 再び普通の Vec<u32> として扱える
    assert_eq!(counts, vec![111, 121, 131, 141]);

    println!("{counts:?}");
}

Rust 1.98.0では次のように実行できます。

$ rustc --version
rustc 1.98.0 (...)

$ cargo run
[111, 121, 131, 141]

ここでは各スレッドが別々の要素を更新していますが、例の目的は高速化ではありません。Vec<u32> の所有権を別のコンテナへ移したり、全要素を AtomicU32 として作り直したりせず、既存のメモリへatomic viewを作れることを確認しています。

Ordering::Relaxed を選んでいるのは、今回必要なのが各カウンタに対するatomicなread-modify-writeだけだからです。別のデータとのhappens-before関係まで必要な処理なら、orderingはその同期要件から決める必要があります。

そして、atomic区間を { ... } で囲んでいるのは見た目だけの話ではありません。同期を必要とする区間をborrowの寿命としてコード上に出しています。

この設計なら、「この処理の間はatomicとして扱う」という意図がかなり明確になります。

通常参照を同時に残そうとすると止められる

では、atomic viewを作りながら元の値も通常参照で触ろうとするとどうなるでしょうか。

次のコードはコンパイルさせないための例です。

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

fn main() {
    let mut counts = vec![10_u32, 20, 30];

    let atomic_counts = AtomicU32::from_mut_slice(&mut counts);

    // atomic_counts が後で使われるため、
    // counts の mutable borrow はまだ生きている
    println!("{}", counts[0]);

    atomic_counts[0].fetch_add(1, Ordering::Relaxed);
}

from_mut_slice(&mut counts) によるmutable borrowがまだ生きているため、途中の counts[0] という通常アクセスはborrow checkerに拒否されます。

重要なのは、atomic operationだからコンパイラが特別に競合検出しているわけではないことです。

from_mut_slice の入口を &mut にしたことで、

元データへの通常アクセス

と

変換後のatomic view

を同時に利用できない形にしています。

ここで unsafe なcastを自分で書いていたら、この境界を自分で証明しなければなりません。stableなAPIとして提供された意味は、単に数行短く書けることより、aliasingの条件を通常のborrowとして表現できることにあります。

ただし、from_mut_slice を使えば並行処理全体が自動的に正しくなるわけではありません。atomic operationのordering、どのスレッドがいつ処理を開始・終了するか、他の共有データとの同期は別の問題です。

また、データが生存期間の大部分で複数スレッドから共有されるなら、最初から Vec<AtomicU32> を持つほうが型として実態に合っています。from_mut_slice が向くのは、通常データとして扱う期間とatomic operationを行う期間を排他的borrowで明確に分離できるケースです。同期区間が入り組んでいるなら、viewへ変換することより、そもそもの所有権と共有範囲を整理したほうが安全です。

参考

Rust 1.98.0 リリース告知

std::sync::atomic::Atomic

core::sync::atomic::AtomicU32

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?