Shared State di Rust: RwLock, Atomic, dan Scoped Thread
Di halaman Konkurensi, kamu berbagi sebuah counter antar-thread dengan Arc<Mutex<T>>: Arc membuat banyak thread bisa memiliki nilai itu, dan Mutex memastikan hanya satu thread yang menyentuhnya pada satu waktu. Di halaman Channel, kamu malah menghindari berbagi data sama sekali dengan saling mengirim pesan. Keduanya alat yang hebat, dan kamu bisa membangun hampir apa saja dengan keduanya.
Tapi bayangkan Mutex seperti satu-satunya kunci toilet di kantor. Memang berhasil, tapi berlebihan kalau yang kamu mau cuma membaca papan pengumuman di lorong. Standard library Rust punya beberapa alat lain, dan masing-masing lebih cocok untuk pekerjaan tertentu. Di halaman ini kamu akan berkenalan dengan empat di antaranya: scoped thread, RwLock, atomic, dan OnceLock/LazyLock. Kamu juga akan belajar cara menghindari satu bug yang tidak bisa ditangkap compiler untukmu: deadlock.
Shared state (state bersama) artinya data yang dipakai bersama oleh beberapa thread sekaligus. Itulah topik halaman ini.
Scoped thread: meminjam, bukan memindahkan
Section titled “Scoped thread: meminjam, bukan memindahkan”Dengan thread::spawn, kamu biasanya butuh move dan sering juga Arc. Alasannya, thread yang di-spawn bisa saja masih berjalan setelah main sudah lanjut ke hal lain, jadi Rust tidak mengizinkannya meminjam variabel lokalmu.
std::thread::scope mengatasi ini dengan sebuah janji: setiap thread yang dimulai di dalam scope pasti selesai sebelum scope itu berakhir. Karena thread-thread itu tidak mungkin hidup lebih lama daripada datamu, mereka boleh langsung meminjamnya. Tanpa Arc, tanpa move, tanpa clone.
use std::thread;
fn main() { let shop = "Toko Pojok"; let sales = vec![12, 7, 30, 5, 18, 22, 9, 14]; let (morning, afternoon) = sales.split_at(4);
let (am, pm) = thread::scope(|s| { let am = s.spawn(|| morning.iter().sum::<i32>()); let pm = s.spawn(|| afternoon.iter().sum::<i32>()); (am.join().unwrap(), pm.join().unwrap()) });
println!("{shop}: pagi {am}, sore {pm}, sehari penuh {}", am + pm); println!("masih milikku: {:?}", sales);}Output:
Toko Pojok: pagi 54, sore 63, sehari penuh 117masih milikku: [12, 7, 30, 5, 18, 22, 9, 14]Inilah yang terjadi:
thread::scope(|s| { ... })membuka scope-nya. Kamu memulai thread dengans.spawn(...), bukanthread::spawn(...).- Kedua closure meminjam
morningdanafternoon, yang hanyalah slice darisales. Tidak ada yang disalin. - Apa pun yang dikembalikan closure milik scope (di sini sebuah tuple berisi dua jumlah) menjadi nilai dari
thread::scope. - Setelah scope selesai,
salesmasih milikmain, jadi kamu tetap bisa memakainya.
Bahkan kalau kamu lupa memanggil join(), scope tetap menunggu semua thread-nya di } penutup. Janji itulah yang membuat peminjaman ini aman.
RwLock: banyak pembaca atau satu penulis
Section titled “RwLock: banyak pembaca atau satu penulis”Bayangkan papan menu di sebuah kafe. Banyak pelanggan bisa membacanya bersamaan, dan itu tidak masalah. Tapi saat pemiliknya mengganti harga, semua orang harus menunggu sebentar sampai papan barunya terpasang.
Itulah RwLock (“read-write lock”, kunci baca-tulis). Ada dua cara masuk:
read()memberimu read guard yang shared (bersama). Banyak thread bisa memegangnya pada saat yang sama.write()memberimu write guard yang exclusive (eksklusif). Selama ada penulis yang memegangnya, tidak ada orang lain yang bisa membaca atau menulis.
Mutex tidak membedakan keduanya: bahkan dua pembaca pun harus bergantian. Berikut cache pengaturan yang dibaca oleh beberapa request handler, lalu diubah sekali oleh admin:
use std::sync::{Arc, RwLock};use std::thread;
struct Settings { theme: String, max_upload_mb: u32,}
fn main() { let settings = Arc::new(RwLock::new(Settings { theme: String::from("light"), max_upload_mb: 10, }));
// Empat request handler membaca pengaturan secara bersamaan. let mut handles = Vec::new(); for id in 1..=4 { let settings = Arc::clone(&settings); handles.push(thread::spawn(move || { let s = settings.read().unwrap(); // shared: thread lain juga bisa membaca format!("request {id}: tema={}, batas={}MB", s.theme, s.max_upload_mb) })); } for handle in handles { println!("{}", handle.join().unwrap()); }
// Admin mengubah pengaturan sekali. { let mut s = settings.write().unwrap(); // exclusive: semua yang lain menunggu s.theme = String::from("dark"); s.max_upload_mb = 25; } // write guard di-drop di sini, jadi kuncinya bebas lagi
let s = settings.read().unwrap(); println!("setelah diubah: tema={}, batas={}MB", s.theme, s.max_upload_mb);}Output:
request 1: tema=light, batas=10MBrequest 2: tema=light, batas=10MBrequest 3: tema=light, batas=10MBrequest 4: tema=light, batas=10MBsetelah diubah: tema=dark, batas=25MBThread-thread itu bisa selesai dalam urutan apa pun, tapi output-nya selalu sama. Setiap thread mengembalikan barisnya alih-alih mencetaknya, dan main mencetak baris-baris itu sesuai urutan ia melakukan join.
Kapan RwLock lebih unggul daripada Mutex? Saat pembacaan sering terjadi, penulisan jarang, dan setiap pembacaan butuh waktu agak lama (misalnya menelusuri cache yang besar). Dalam kondisi itu, para pembaca benar-benar berjalan berdampingan.
Kapan tidak? Kalau pembacaannya sangat singkat, atau penulisan terjadi terus-menerus, RwLock hanyalah Mutex yang lebih rumit. Ia melakukan pencatatan tambahan, dan pembaca serta penulis kebanyakan tetap saling menunggu. Kalau tidak yakin, mulailah dengan Mutex. Lebih sederhana, dan kamu bisa beralih nanti kalau pengukuran menunjukkan bahwa kunci itu memang jadi bottleneck.
Atomic: counter tanpa kunci
Section titled “Atomic: counter tanpa kunci”Untuk satu angka atau satu flag true/false, kunci pun sebenarnya berlebihan. Tipe atomic seperti AtomicUsize, AtomicI64, dan AtomicBool bisa diubah dengan aman oleh banyak thread sekaligus, tanpa kunci sama sekali. CPU menjalankan setiap operasinya dalam satu langkah yang tidak bisa dipecah, jadi tidak ada update yang hilang.
Di sini, 8 thread masing-masing mencatat 1000 tayangan halaman. Mereka juga menghitung vote: satu upvote, lalu tiga downvote, begitu terus berulang-ulang. Total vote-nya jadi negatif, karena itulah tipenya AtomicI64, bukan tipe unsigned:
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};use std::thread;
fn main() { let page_views = AtomicUsize::new(0); let net_votes = AtomicI64::new(0);
thread::scope(|s| { for _ in 0..8 { s.spawn(|| { for n in 0..1000 { page_views.fetch_add(1, Ordering::Relaxed); if n % 4 == 0 { net_votes.fetch_add(1, Ordering::Relaxed); // upvote } else { net_votes.fetch_sub(1, Ordering::Relaxed); // downvote } } }); } }); // kedelapan thread sudah selesai di sini
println!("tayangan halaman: {}", page_views.load(Ordering::Relaxed)); println!("total vote: {}", net_votes.load(Ordering::Relaxed));}Output:
tayangan halaman: 8000total vote: -4000Perhatikan bahwa page_views tidak mut. Atomic diubah lewat referensi bersama, dan justru karena itulah banyak thread bisa memakainya sekaligus. Method yang paling sering kamu pakai:
fetch_add(n, ordering)/fetch_sub(n, ordering)menambah atau mengurangi, lalu mengembalikan nilai yang lama.load(ordering)membaca nilai saat ini.store(value, ordering)menimpanya.
Apa itu Ordering?
Section titled “Apa itu Ordering?”Setiap operasi atomic meminta sebuah Ordering. Ini memberi tahu CPU dan compiler seberapa ketat operasi ini harus tetap berurutan terhadap operasi memori di sekitarnya. Penjelasan lengkapnya cukup dalam, tapi sebagai pemula kamu hanya butuh dua aturan:
- Kalau tidak yakin, pakai
Ordering::SeqCst. Ini pilihan yang paling ketat dan paling aman. Ordering::Relaxedsudah cukup untuk counter sederhana yang hanya kamu baca di akhir, seperti contoh di atas. Totalnya tetap tepat.
Kalau suatu hari kamu ingin mendalaminya, buku gratis Rust Atomics and Locks (tautannya ada di bagian bawah) menjelaskan ordering dengan jelas.
Flag untuk berhenti
Section titled “Flag untuk berhenti”AtomicBool sangat cocok untuk sinyal “tolong berhenti”. Di sini sebuah worker di latar belakang terus melakukan autosave sampai main membalik flag-nya:
use std::sync::atomic::{AtomicBool, Ordering};use std::thread;use std::time::Duration;
fn main() { let stop = AtomicBool::new(false);
let report = thread::scope(|s| { let worker = s.spawn(|| { while !stop.load(Ordering::SeqCst) { // anggap saja dokumen di-autosave di sini thread::sleep(Duration::from_millis(5)); } "worker autosave berhenti dengan rapi" });
thread::sleep(Duration::from_millis(30)); // main mengerjakan hal lain stop.store(true, Ordering::SeqCst); // minta worker berhenti worker.join().unwrap() });
println!("{report}");}Output:
worker autosave berhenti dengan rapiWorker memeriksa flag itu setiap kali perulangannya berputar. Ia menyelesaikan langkah yang sedang dikerjakan lalu keluar sendiri, jauh lebih baik daripada mematikan thread di tengah-tengah proses menyimpan file.
OnceLock dan LazyLock: siapkan sekali saja
Section titled “OnceLock dan LazyLock: siapkan sekali saja”Kadang kamu ingin satu nilai global yang bisa dibaca semua thread: tabel pencarian, nama aplikasi, atau konfigurasi yang dimuat saat program mulai. Bagian sulitnya adalah membangun nilai itu. Bagaimana kalau dua thread mencoba membangunnya di saat yang sama?
std::sync::LazyLock dan std::sync::OnceLock mengatasi masalah ini. Nilainya dibuat tepat satu kali, yaitu saat pertama kali ada yang membutuhkannya. Kalau ada thread lain yang meminta selagi nilainya masih dibangun, mereka cukup menunggu, lalu semuanya memakai nilai yang sama.
use std::collections::HashMap;use std::sync::{LazyLock, OnceLock};use std::thread;
// LazyLock: resepnya ditulis langsung di sini, dan dijalankan saat pertama dipakai.static CURRENCIES: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| { println!("membangun tabel mata uang..."); HashMap::from([("US", "USD"), ("JP", "JPY"), ("DE", "EUR"), ("BR", "BRL")])});
// OnceLock: awalnya kosong, dan kamu menentukan cara mengisinya di tempat ia dipakai.static STORE_NAME: OnceLock<String> = OnceLock::new();
fn store_name() -> &'static str { STORE_NAME.get_or_init(|| { std::env::var("STORE_NAME").unwrap_or_else(|_| String::from("Toko Rusty")) })}
fn main() { let mut handles = Vec::new(); for country in ["US", "JP", "DE", "BR"] { handles.push(thread::spawn(move || { format!("{} di {country} memakai mata uang {}", store_name(), CURRENCIES[country]) })); } for handle in handles { println!("{}", handle.join().unwrap()); }}Output:
membangun tabel mata uang...Toko Rusty di US memakai mata uang USDToko Rusty di JP memakai mata uang JPYToko Rusty di DE memakai mata uang EURToko Rusty di BR memakai mata uang BRLEmpat thread menginginkan tabel itu di waktu yang hampir bersamaan, tapi “membangun tabel mata uang…” hanya tercetak sekali. Dan karena ini adalah nilai static yang hidup selama program berjalan, thread-thread itu sama sekali tidak butuh Arc.
Mana yang sebaiknya kamu pilih?
LazyLocksaat kamu tahu cara membangun nilainya di tempat kamu mendeklarasikannya. Rasanya seperti variabel global biasa.OnceLocksaat nilainya baru ditentukan belakangan, misalnya dari flag command-line. Kamu mengisinya denganget_or_init(...)atauset(...)(dansetgagal dengan sopan kalau sudah terisi).
Keduanya bagian dari standard library. LazyLock baru hadir di Rust 1.80, jadi kode yang lebih lama sering memakai crate once_cell atau lazy_static untuk pekerjaan yang sama.
Menghindari deadlock
Section titled “Menghindari deadlock”Compiler mencegah data race, tapi ada satu bug konkurensi yang tidak bisa ia tangkap: deadlock (kebuntuan). Ini terjadi saat thread-thread saling menunggu selamanya dan programmu langsung membeku. Tanpa error, tanpa crash, tanpa apa-apa.
Resep klasiknya adalah dua kunci yang diambil dalam urutan berbeda. Bayangkan dua orang yang sama-sama butuh pulpen dan buku catatan. Yang satu mengambil pulpen, yang lain mengambil buku catatan, dan masing-masing dengan sopan menunggu yang lain melepaskannya. Selamanya.
// Thread A // Thread Blet from = alice.lock().unwrap(); let from = bob.lock().unwrap();let to = bob.lock().unwrap(); let to = alice.lock().unwrap();// A pegang alice, tunggu bob. // B pegang bob, tunggu alice. Macet!Tiga kebiasaan ini menjagamu tetap aman:
- Selalu ambil kunci dalam urutan yang sama. Kalau setiap thread mengunci
alicesebelumbob(misalnya diurutkan berdasarkan ID akun), lingkaran saling tunggu itu tidak mungkin terjadi. - Buat cakupan kunci sesingkat mungkin. Pegang kunci hanya selama yang dibutuhkan untuk membaca atau mengubah data. Jangan pernah memegangnya selama pekerjaan yang lambat seperti panggilan jaringan, menulis file, atau perhitungan yang panjang. Selama itu, semua thread lain terjebak menunggu.
- Drop guard lebih awal. Guard membuka kunci saat ia di-drop. Kamu bisa mengakhirinya dengan blok dalam
{ ... }(seperti penulisRwLockdi atas) atau memanggildrop(guard)sendiri.
Berikut kebiasaan ke-3 dalam praktik. Setiap worker mengambil satu pekerjaan dari antrean bersama, membuka kunci antrean, dan baru setelah itu mengerjakan bagian yang lambat:
use std::sync::Mutex;use std::thread;
fn main() { let queue = Mutex::new(vec!["ubah ukuran foto", "kirim email", "buat laporan"]); let finished = Mutex::new(Vec::new());
thread::scope(|s| { for _ in 0..3 { s.spawn(|| { let mut jobs = queue.lock().unwrap(); let job = jobs.pop(); drop(jobs); // buka kunci antrean SEBELUM bagian yang lambat
if let Some(job) = job { let result = format!("selesai: {job}"); // anggap ini butuh beberapa detik finished.lock().unwrap().push(result); } }); } });
// Scope sudah berakhir, jadi tidak ada thread yang memakai `finished` lagi. let mut done = finished.into_inner().unwrap(); done.sort(); // thread selesai dalam urutan acak for line in done { println!("{line}"); }}Output:
selesai: buat laporanselesai: kirim emailselesai: ubah ukuran fotoTanpa drop(jobs), guard-nya akan hidup sampai akhir closure. Akibatnya pekerjaan yang “lambat” terjadi selagi antrean masih terkunci, dan ketiga worker akan berjalan satu per satu, bukan berdampingan.
Ada satu kasus licik yang perlu kamu tahu: di while let Some(job) = queue.lock().unwrap().pop() { ... }, guard-nya hidup selama seluruh isi perulangan. Jadi antrean tetap terkunci selama kamu mengerjakan job itu. Ambil job-nya dulu di pernyataan let yang terpisah, seperti di contoh.
Alat mana yang kupakai?
Section titled “Alat mana yang kupakai?”| Kamu ingin… | Pakai |
|---|---|
| Mengirim pekerjaan atau hasil dari satu thread ke thread lain | Channel (mpsc) |
| Membuat banyak thread bisa membaca dan mengubah nilai yang sama | Arc<Mutex<T>> |
| Berbagi data yang sering dibaca dan jarang diubah | Arc<RwLock<T>> |
| Menghitung sesuatu, atau berbagi flag nyala/mati sederhana | Atomic (AtomicUsize, AtomicBool, …) |
| Membangun nilai global sekali, lalu hanya membacanya | LazyLock atau OnceLock |
Membuat thread bisa meminjam data lokal, tanpa Arc |
thread::scope |
Semua ini bisa dikombinasikan dengan bebas. Contoh-contoh di halaman ini sudah menggabungkan thread::scope dengan atomic dan dengan Mutex. Pilih alat paling sederhana yang cocok, dan jangan khawatir salah pilih: beralih belakangan biasanya hanya perubahan kecil.
Kesalahan umum
Section titled “Kesalahan umum”Meminjam variabel lokal di thread::spawn. Ini kelihatannya tidak berbahaya:
use std::thread;
fn main() { let sales = vec![12, 7, 30, 5];
let handle = thread::spawn(|| { let total: i32 = sales.iter().sum(); total });
println!("total: {}", handle.join().unwrap());}error[E0373]: closure may outlive the current function, but it borrows `sales`, which is owned by the current function --> src/main.rs:6:32 |6 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `sales`7 | let total: i32 = sales.iter().sum(); | ----- `sales` is borrowed here |help: to force the closure to take ownership of `sales` (and any other referenced variables), use the `move` keywordCompiler bilang closure itu mungkin hidup lebih lama daripada fungsi saat ini, padahal ia meminjam sales yang dimiliki fungsi tersebut. Kamu tahu bahwa kamu langsung memanggil join(), tapi thread::spawn tidak bisa tahu itu. Dari sudut pandangnya, thread itu bisa saja berjalan selamanya. Ada dua cara memperbaikinya: tambahkan move (setelah itu main tidak bisa memakai sales lagi), atau pakai thread::scope, yang boleh meminjam:
let total: i32 = thread::scope(|s| s.spawn(|| sales.iter().sum()).join().unwrap());Lupa memakai Arc. Tanpa Arc, closure move milik thread pertama mengambil Mutex-nya, dan tidak ada yang tersisa untuk thread berikutnya:
use std::sync::Mutex;use std::thread;
fn main() { let counter = Mutex::new(0); let mut handles = vec![];
for _ in 0..3 { handles.push(thread::spawn(move || { *counter.lock().unwrap() += 1; })); }
for handle in handles { handle.join().unwrap(); }}error[E0382]: use of moved value: `counter` --> src/main.rs:9:36 | 5 | let counter = Mutex::new(0); | ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait... 8 | for _ in 0..3 { | ------------- inside of this loop 9 | handles.push(thread::spawn(move || { | ^^^^^^^ value moved into closure here, in previous iteration of loopError ini artinya counter sudah dipindahkan ke closure pada putaran perulangan sebelumnya, jadi tidak bisa dipakai lagi. Bungkus dengan Arc::new(...) dan clone Arc-nya di awal perulangan, seperti di halaman Konkurensi. Atau pakai thread::scope dan hapus move-nya. Kalau kamu memakai Rc alih-alih Arc, kamu akan mendapat error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely (tidak bisa dikirim antar-thread dengan aman). Rc hanya untuk satu thread (lihat Smart pointer).
Latihan
Section titled “Latihan”1. Jumlahkan dalam empat bagian
Section titled “1. Jumlahkan dalam empat bagian”Buat Vec berisi angka 1 sampai 1000. Pakai thread::scope untuk membaginya menjadi 4 bagian berisi 250 angka, lalu jumlahkan setiap bagian di thread-nya sendiri. Cetak jumlah setiap bagian dan total keseluruhannya. Petunjuk: numbers.chunks(250) memberimu slice-slice-nya. Kumpulkan handle-nya ke dalam Vec, lalu join satu per satu sesuai urutan.
Lihat solusi
use std::thread;
fn main() { let numbers: Vec<u64> = (1..=1000).collect();
let sums: Vec<u64> = thread::scope(|s| { let mut handles = Vec::new(); for chunk in numbers.chunks(250) { handles.push(s.spawn(move || chunk.iter().sum::<u64>())); } handles.into_iter().map(|h| h.join().unwrap()).collect() });
println!("jumlah per bagian: {:?}", sums); println!("total: {}", sums.iter().sum::<u64>());}Output:
jumlah per bagian: [31375, 93875, 156375, 218875]total: 500500move di sini hanya memindahkan chunk, yaitu slice pinjaman yang kecil, ke setiap thread. Vektornya sendiri tetap dipinjam, bukan dipindahkan, jadi kamu masih bisa memakai numbers lagi setelah scope selesai.
2. Nomor pesanan yang unik
Section titled “2. Nomor pesanan yang unik”Sebuah toko online memberi setiap pesanan sebuah nomor, mulai dari 1000. Jalankan 4 thread yang masing-masing mengambil 3 nomor pesanan dari satu AtomicU64 bersama. Kumpulkan ke-12 nomor itu, urutkan, lalu cetak. Petunjuk: fetch_add(1, Ordering::SeqCst) mengembalikan nilai sebelum ditambah, jadi setiap nomor dibagikan tepat sekali.
Lihat solusi
use std::sync::atomic::{AtomicU64, Ordering};use std::thread;
fn main() { let next_order = AtomicU64::new(1000);
let mut numbers: Vec<u64> = thread::scope(|s| { let mut handles = Vec::new(); for _ in 0..4 { handles.push(s.spawn(|| { (0..3) .map(|_| next_order.fetch_add(1, Ordering::SeqCst)) .collect::<Vec<u64>>() })); } handles.into_iter().flat_map(|h| h.join().unwrap()).collect() });
numbers.sort(); println!("{} pesanan: {:?}", numbers.len(), numbers);}Output:
12 pesanan: [1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011]Thread mana yang mendapat nomor berapa bisa berubah di setiap run, tapi tidak ada nomor yang dibagikan dua kali atau terlewat. Pengurutan membuat daftar yang dicetak selalu sama.
Ringkasan
Section titled “Ringkasan”thread::scopemenjamin semua thread-nya selesai sebelum scope berakhir, jadi thread-thread itu bisa meminjam data lokal tanpaArcataumove.RwLockmengizinkan banyak pembaca atau satu penulis. Ia membantu untuk data yang sering dibaca; selain itu,Mutexlebih sederhana.- Atomic (
AtomicUsize,AtomicI64,AtomicBool) menangani counter dan flag tanpa kunci. PakaiSeqCstkalau ragu,Relaxeduntuk counter sederhana. LazyLockdanOnceLockmembuat nilai global tepat satu kali, dengan aman, bahkan saat banyak thread memintanya bersamaan.- Hindari deadlock: ambil kunci dalam urutan yang sama, buat cakupan kunci singkat, dan drop guard lebih awal.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kenapa thread yang dimulai dengan s.spawn di dalam thread::scope boleh meminjam variabel lokal?
2. Pernyataan mana tentang RwLock yang benar?
3. Kamu butuh counter tayangan halaman yang dipakai bersama oleh banyak thread. Apa pilihan bagus yang paling sederhana?
4. Thread A mengunci alice lalu bob. Thread B mengunci bob lalu alice. Apa yang bisa terjadi?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Shared-State Concurrency — The Rust Book (bahasa Inggris): bab resmi tentang
MutexdanArc, dasar yang dikembangkan di halaman ini. std::thread::scope— dokumentasi standard library (bahasa Inggris): cara kerja scoped thread, dengan contoh tambahan.std::sync::RwLock— dokumentasi standard library (bahasa Inggris): read guard dan write guard, poisoning, dan catatan tentang bagaimana keadilan antrean kunci bergantung pada sistem operasi.std::sync::atomic— dokumentasi standard library (bahasa Inggris): semua tipe atomic dan ringkasan singkat tentang memory ordering.- Rust Atomics and Locks karya Mara Bos (bahasa Inggris): buku online gratis yang menjelaskan atomic, memory ordering, dan cara kunci dibangun, langkah demi langkah.