Rustにおけるメモリ注文:安全な並行処理のガイド
Lukas Schneider
DevOps Engineer · Leapcell

並行プログラミングにおいて、メモリ操作の順序を正しく管理することは、プログラムの正確性を保証する上で重要です。Rustは、アトミック操作とOrdering
列挙型を提供し、開発者がマルチスレッド環境で共有データを安全かつ効率的に操作できるようにします。この記事では、RustにおけるOrdering
の原則と使用方法について詳しく解説し、開発者がこの強力なツールをより良く理解し、活用できるよう支援することを目的としています。
メモリ順序の基礎
最新のプロセッサとコンパイラは、パフォーマンスを最適化するために命令とメモリ操作の順序を並べ替えます。通常、この並べ替えはシングルスレッドプログラムでは問題を引き起こしませんが、適切に制御されていない場合、マルチスレッド環境ではデータ競合や不整合な状態を引き起こす可能性があります。この問題に対処するために、メモリ順序の概念が導入され、開発者はアトミック操作のメモリ順序を指定して、並行環境でのメモリアクセスの正確な同期を保証できるようになりました。
RustのOrdering
列挙型
Rustの標準ライブラリにあるOrdering
列挙型は、さまざまなレベルのメモリ順序の保証を提供し、開発者は特定のニーズに基づいて適切な順序モデルを選択できます。以下は、Rustで使用可能なメモリ順序オプションです。
Relaxed
Relaxed
は最も基本的な保証を提供します。単一のアトミック操作のアトミック性を保証しますが、操作の順序は保証しません。これは、操作の相対的な順序がプログラムの正確性に影響を与えない単純なカウントまたは状態マーキングに適しています。
AcquireとRelease
Acquire
とRelease
は、操作の部分的な順序を制御します。Acquire
は、現在のスレッドが後続の操作を実行する前に、一致するRelease
操作によって行われた変更を確認することを保証します。これらは通常、ロックやその他の同期プリミティブを実装するために使用され、リソースがアクセス前に適切に初期化されることを保証します。
AcqRel
AcqRel
は、Acquire
とRelease
の効果を組み合わせたもので、値の読み取りと変更の両方を行う操作に適しており、これらの操作が他のスレッドとの相対的な順序で実行されることを保証します。
SeqCst
SeqCst
(シーケンシャルコンシステンシー)は、最も強力な順序保証を提供します。すべてのスレッドが同じ順序で操作を確認することを保証し、グローバルに一貫した実行順序が必要なシナリオに適しています。
Ordering
の実用的な使用法
適切なOrdering
を選択することが重要です。順序が緩すぎると、プログラムに論理的なエラーが発生する可能性があり、順序が厳しすぎると、パフォーマンスが不必要に低下する可能性があります。以下は、Ordering
の使用法を示すRustのコード例です。
例1:マルチスレッド環境での順序付きアクセスにRelaxed
を使用する
この例では、マルチスレッド環境で単純なカウント操作にRelaxed
順序を使用する方法を示します。
use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread; let counter = AtomicUsize::new(0); thread::spawn(move || { counter.fetch_add(1, Ordering::Relaxed); }).join().unwrap(); println!("Counter: {}", counter.load(Ordering::Relaxed));
- ここでは、
AtomicUsize
型の原子カウンターcounter
が作成され、0に初期化されています。 thread::spawn
を使用して新しいスレッドが生成され、その中でfetch_add
操作がカウンターに対して実行され、その値が1ずつインクリメントされます。Ordering::Relaxed
は、インクリメント操作がアトミックに実行されることを保証しますが、操作の順序は保証しません。これは、複数のスレッドが同時にcounter
に対してfetch_add
を実行すると、すべての操作が安全に完了しますが、それらの実行順序は予測不可能になることを意味します。Relaxed
は、操作の特定の順序ではなく、最終的なカウントのみを気にする単純なカウントシナリオに適しています。
例2:Acquire
とRelease
を使用してデータアクセスを同期する
この例では、Acquire
とRelease
を使用して、2つのスレッド間でデータアクセスを同期する方法を示します。
use std::sync::{Arc, atomic::{AtomicBool, Ordering}}; use std::thread; let data_ready = Arc::new(AtomicBool::new(false)); let data_ready_clone = Arc::clone(&data_ready); // Producer thread thread::spawn(move || { // Prepare data // ... data_ready_clone.store(true, Ordering::Release); }); // Consumer thread thread::spawn(move || { while !data_ready.load(Ordering::Acquire) { // Wait until data is ready } // Safe to access the data prepared by producer });
- ここでは、データが準備完了かどうかを示すために、
AtomicBool
フラグdata_ready
が作成され、false
に初期化されています。 Arc
は、複数のスレッド間でdata_ready
を安全に共有するために使用されます。- プロデューサースレッドはデータを準備し、
Ordering::Release
を指定してstore
メソッドを使用してdata_ready
をtrue
に更新し、データが準備完了であることを示します。 - コンシューマースレッドは、
data_ready
の値がtrue
になるまで、ループ内でOrdering::Acquire
を指定してload
メソッドを使用してdata_ready
を継続的にチェックします。- ここでは、
Acquire
とRelease
を組み合わせて使用して、data_ready
をtrue
に設定する前にプロデューサーによって実行されたすべての操作が、コンシューマースレッドが準備されたデータにアクセスする前に見えるようにします。
- ここでは、
例3:読み取り-変更-書き込み操作にAcqRel
を使用する
この例では、読み取り-変更-書き込み操作中に正確な同期を保証するためにAcqRel
を使用する方法を示します。
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}}; use std::thread; let some_value = Arc::new(AtomicUsize::new(0)); let some_value_clone = Arc::clone(&some_value); // Modification thread thread::spawn(move || { // Here, `fetch_add` both reads and modifies the value, so `AcqRel` is used some_value_clone.fetch_add(1, Ordering::AcqRel); }).join().unwrap(); println!("some_value: {}", some_value.load(Ordering::SeqCst));
AcqRel
はAcquire
とRelease
の組み合わせであり、データの読み取り(獲得)と変更(解放)の両方を行う操作に適しています。- この例では、
fetch_add
は読み取り-変更-書き込み(RMW)操作です。最初にsome_value
の現在の値を読み取り、次に1ずつインクリメントし、最後に新しい値を書き戻します。この操作により、以下が保証されます。- 読み取られた値は最新のものであり、(他のスレッドで行われた可能性のある)以前のすべての変更が現在のスレッドに表示されます(Acquireセマンティクス)。
some_value
への変更は、他のスレッドにすぐに表示されます(Releaseセマンティクス)。
AcqRel
を使用すると、次のことが保証されます。fetch_add
の前の読み取りまたは書き込み操作は、その後に並べ替えられません。fetch_add
の後の読み取りまたは書き込み操作は、その前に並べ替えられません。- これにより、
some_value
を変更する際の正確な同期が保証されます。
例4:グローバルな順序を保証するためにSeqCst
を使用する
この例では、操作のグローバルに一貫した順序を保証するためにSeqCst
を使用する方法を示します。
use std::sync::atomic::{AtomicUsize, Ordering}}; use std::thread; let counter = AtomicUsize::new(0); thread::spawn(move || { counter.fetch_add(1, Ordering::SeqCst); }).join().unwrap(); println!("Counter: {}", counter.load(Ordering::SeqCst));
- 例1と同様に、これもカウンターに対してアトミックなインクリメント操作を実行します。
- 違いは、ここでは
Ordering::SeqCst
が使用されていることです。SeqCst
は最も厳密なメモリ順序であり、個々の操作のアトミック性だけでなく、グローバルに一貫した実行順序も保証します。 SeqCst
は、次のような強力な整合性が必要な場合にのみ使用する必要があります。- 時刻同期、
- マルチプレイヤーゲームでの同期、
- ステートマシンの同期など
SeqCst
を使用すると、すべてのスレッドのすべてのSeqCst
操作が、単一のグローバルに合意された順序で実行されるように見えます。これは、操作の正確なシーケンスを維持する必要があるシナリオで役立ちます。
Rustプロジェクトのホスティングに最適なLeapcellをご紹介します。
Leapcellは、ウェブホスティング、非同期タスク、Redisのための次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発できます。
無制限のプロジェクトを無料でデプロイ
- 使用量に応じてのみ料金が発生します — リクエストも料金もありません。
他に類を見ないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOpsの統合。
- 実用的な洞察のためのリアルタイムのメトリクスとロギング。
簡単な拡張性と高いパフォーマンス
- 高い並行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください: @LeapcellHQ