Rust'ta birden çok iş parçacığının aynı veriyi değiştirmesi gerektiğinde standart çözüm Arc<Mutex<T>> birleşimidir: Arc (atomically reference counted) verinin birden çok iş parçacığı tarafından sahiplenilmesini sağlar, Mutex (mutual exclusion) ise aynı anda yalnızca bir iş parçacığının veriye erişmesini garanti eder. Veriye ulaşmak için lock() çağırırsınız; dönen MutexGuard kapsamdan çıktığında kilit kendiliğinden açılır. Çok okunup az yazılan veri için RwLock, basit sayaç ve bayraklar için std::sync::atomic tipleri daha uygun olabilir.
Bu yazıda Mutex API'sini, Rc yerine neden Arc gerektiğini, kilidin ne kadar süre tutulduğunu belirleyen kuralları, RwLock ve atomiklerin farkını, panik sonrası zehirlenmeyi (poisoning) ve tüm bunların arkasındaki Send ile Sync trait'lerini çalıştırılmış örneklerle anlatıyoruz. İş parçacığı açmayı Rust thread::spawn, mesajlaşmayı Rust mpsc kanalları yazısında ele aldık; seri sırası için Rust öğrenme rehberi sayfasına bakabilirsiniz.
Mutex<T> API: lock, MutexGuard ve otomatik kilit açma

Kitap, mutex kullanmanın zor diye bilinmesini iki kurala bağlar: veriyi kullanmadan önce kilidi almanız ve işiniz bitince kilidi bırakmanız gerekir. Rust'ta ikisi de tip sistemine gömülüdür. Mutex<i32> tipindeki bir değer i32 değildir; içindeki sayıya ulaşmanın tek yolu lock çağırmaktır. lock, bir LockResult<MutexGuard<'_, T>> döndürür. Guard Deref ve DerefMut uygulayarak içteki veriye referans gibi davranır, Drop uygulaması da guard düştüğünde kilidi açar. Yani kilidi açmayı unutmak mümkün değildir.
use std::sync::Mutex;
fn main() {
let stok = Mutex::new(5);
{
let mut kilit = stok.lock().unwrap(); // MutexGuard<i32>
*kilit -= 2;
} // kilit burada düşer, mutex serbest kalır
*stok.lock().unwrap() += 10; // geçici guard ifadenin sonunda düşer
println!("{stok:?}");
println!("{}", stok.into_inner().unwrap());
}
// çıktı: Mutex { data: 13, poisoned: false, .. }
// çıktı: 13lock çağrısı, kilit başka bir iş parçacığındaysa mevcut iş parçacığını kilit boşalana kadar bloklar. Beklemek istemiyorsanız try_lock kullanırsınız: kilit o an alınamıyorsa WouldBlock hatası döner. Değeri mutex'ten tamamen çıkarmak için into_inner, elinizde &mut Mutex<T> varken kilit almadan erişmek için get_mut vardır; değiştirilebilir referans zaten tekil erişimi kanıtladığı için kilide gerek kalmaz. Örnekteki Debug çıktısında görülen poisoned: false alanı aşağıda anlatılan zehirlenme durumunu gösterir.
Neden Rc değil de Arc?
Kitaptaki örnekte on iş parçacığı aynı sayacı artırmaya çalışır. Mutex'i doğrudan move closure'a verirseniz ilk döngü adımında sayacın sahipliği taşınır ve ikinci adımda E0382 alırsınız: aynı değeri birden çok iş parçacığına taşıyamazsınız. Çoklu sahiplik için akla ilk gelen Rc<T> ise iş parçacıkları arasında kullanılamaz:
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let sayac = Rc::new(Mutex::new(0));
let kopya = Rc::clone(&sayac);
let t = thread::spawn(move || {
*kopya.lock().unwrap() += 1;
});
t.join().unwrap();
}
// error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safelyHatanın gerekçesi, derleyicinin de belirttiği gibi Rc<Mutex<i32>> tipinin Send uygulamamasıdır. Rc, referans sayısını klonlarda artırıp düşürmelerde azaltırken bunu herhangi bir eşzamanlılık önlemi olmadan yapar; iki iş parçacığı sayacı aynı anda değiştirirse sayı bozulabilir, bu da bellek sızıntısına ya da verinin erken düşürülmesine yol açar. Arc<T> aynı API'ye sahiptir ama sayacı atomik işlemlerle günceller. Kitap, tüm tiplerin varsayılan olarak atomik olmamasının nedenini de açıklar: iş parçacığı güvenliğinin bir performans bedeli vardır ve bu bedeli yalnızca gerçekten gerektiğinde ödemek istersiniz. Rc ve tek iş parçacıklı karşılığı RefCell için Rust Rc ve RefCell yazısına bakın.
Arc<Mutex<T>> ile ortak durumu güncellemek
Doğru desen şudur: Arc::new(Mutex::new(...)) ile veriyi bir kez oluşturur, her iş parçacığına Arc::clone(&x) ile bir kopya verirsiniz. Klonlama veriyi değil, yalnızca işaretçiyi kopyalar ve referans sayısını bir artırır. Aşağıdaki örnekte altı iş parçacığı ortak bir HashMap içinde kelime sayıyor.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let sayimlar = Arc::new(Mutex::new(HashMap::new()));
let kelimeler = ["elma", "armut", "elma", "çilek", "elma", "armut"];
let mut tutamaclar = Vec::new();
for k in kelimeler {
let sayimlar = Arc::clone(&sayimlar); // sayaç artar, veri kopyalanmaz
tutamaclar.push(thread::spawn(move || {
let mut harita = sayimlar.lock().unwrap();
*harita.entry(k).or_insert(0) += 1;
}));
}
for t in tutamaclar {
t.join().unwrap();
}
// Tüm kopyalar iş parçacıklarıyla birlikte düştü, yalnızca bizimki kaldı.
println!("güçlü referans: {}", Arc::strong_count(&sayimlar));
let harita = sayimlar.lock().unwrap();
let mut liste: Vec<_> = harita.iter().collect();
liste.sort();
println!("{liste:?}");
}
// çıktı: güçlü referans: 1
// çıktı: [("armut", 2), ("elma", 3), ("çilek", 1)]Tüm iş parçacıkları join edildikten sonra Arc::strong_count 1 gösteriyor, çünkü closure'lara taşınan klonlar iş parçacıklarıyla birlikte düştü. Belgedeki önemli bir uyarı: Arc<T> yalnızca T hem Send hem Sync ise Send ve Sync olur. Yani Arc içine koyduğunuz veriyi kendiliğinden iş parçacığı güvenli hale getirmez; sadece sahipliğin paylaşılmasını güvenli kılar. Arc<RefCell<T>> bu yüzden iş parçacıkları arasında kullanılamaz ve Arc genellikle bir std::sync tipiyle eşleştirilir. Yalnızca okunacak veriler için kilit gerekmez; Arc<Vec<T>> gibi bir değeri doğrudan paylaşabilirsiniz. Sıralamada çilek kelimesinin sona düşmesinin nedeni, string karşılaştırmasının bayt değerlerine göre yapılmasıdır; ayrıntı Rust String ve &str yazısında.
Kilit ne kadar süre tutulur?

Kilit, guard yaşadığı sürece tutulur. Bu basit kural pratikte üç farklı biçimde karşınıza çıkar ve yanlış anlaşıldığında ya paralellik kaybolur ya da program kilitlenir:
- Adlandırılmış guard:
let mut g = m.lock().unwrap();yazdığınızda kilit,gkapsamın sonuna kadar yaşar. Daha erken bırakmak için iç bir blok açın ya dadrop(g)çağırın; std belgesindeki örnek tam olarak bunu önerir. - Geçici guard:
*m.lock().unwrap() += 1;satırında guard bir geçici değerdir ve deyimin sonunda düşer. Kısa güncellemeler için en güvenli yazımdır. - if let, while let ve match: bu yapıların koşul kısmında oluşan geçici değerler ilgili bloğun sonuna kadar yaşar. Kitabın iş parçacığı havuzu bölümü,
while letile yazılan bir işçinin iş boyunca kilidi tuttuğunu ve diğer işçileri beklettiğini gösterir; bu örneği mpsc kanalları yazısında çalıştırdık.
Kilitlenme (deadlock) Rust'ın engelleyebildiği bir hata değildir. Kitap, iki kaynağı kilitlemesi gereken bir işlemde iki iş parçacığının birer kilidi alıp birbirini sonsuza kadar beklemesini tipik örnek olarak verir. Bundan korunmanın yaygın yolu, birden çok kilit gerekiyorsa onları her yerde aynı sırayla almaktır. Ayrıca aynı iş parçacığında, kilidi zaten tutarken aynı mutex'i yeniden kilitlemeye çalışmak belgede tanımsız bırakılmıştır: lock ikinci çağrıda dönmez, panik yapabilir ya da kilitlenebilir. Bir fonksiyon guard tutarken aynı mutex'i kilitleyen başka bir fonksiyonu çağırıyorsa bu hataya düşersiniz.
RwLock: çok okuyucu, tek yazıcı
Mutex okuyan ile yazanı ayırt etmez, her erişimi sıraya sokar. RwLock<T> ise aynı anda istediğiniz kadar okuyucuya ya da tek bir yazıcıya izin verir. read() paylaşılan erişim için RwLockReadGuard, write() tekil erişim için RwLockWriteGuard döndürür. Belgeye göre T tipinin iş parçacıkları arasında paylaşılması için Send, okuyucuların eşzamanlı erişimi için Sync olması gerekir.
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let ayarlar = Arc::new(RwLock::new(vec![String::from("tr")]));
// Aynı anda birden çok okuyucu kilidi alınabilir.
{
let r1 = ayarlar.read().unwrap();
let r2 = ayarlar.read().unwrap();
println!("okuyucular: {} ve {}", r1[0], r2[0]);
// Okuyucular açıkken yazma kilidi hemen alınamaz.
println!("try_write: {}", ayarlar.try_write().is_err());
}
let yazici = Arc::clone(&ayarlar);
thread::spawn(move || {
yazici.write().unwrap().push(String::from("en"));
})
.join()
.unwrap();
println!("{:?}", *ayarlar.read().unwrap());
}
// çıktı: okuyucular: tr ve tr
// çıktı: try_write: true
// çıktı: ["tr", "en"]Belgedeki iki uyarıyı bilmek önemlidir. Birincisi, kilidin öncelik politikası işletim sistemine bağlıdır ve belirli bir politika garanti edilmez; bekleyen bir yazıcı yeni okuma isteklerini engelleyebilir de, engellemeyebilir de. Bu yüzden aynı iş parçacığında bir okuma kilidi tutarken ikinci kez read() çağırmak, arada başka bir iş parçacığı write() bekliyorsa kilitlenmeye yol açabilir. İkincisi, RwLock yalnızca yazma kilidi tutulurken oluşan panikte zehirlenir; okuyucudaki panik kilidi zehirlemez. Okumaların yazmalardan çok daha sık olduğu yapılandırma ya da önbellek verisi için RwLock uygun bir seçimdir; aksi halde Mutex daha sade kalır.
Atomik tipler ve static değişkenler
Tek bir sayıyı artırmak ya da bir bayrağı açıp kapatmak için tam bir mutex gerekmez. std::sync::atomic modülü AtomicBool, AtomicUsize, AtomicI64 gibi tipler sunar; bunlar Sync olduğu için doğrudan paylaşılabilir ve fetch_add, load, store, compare_exchange gibi işlemlerle güncellenir. Belgeye göre mevcut oldukları platformlarda kilitsiz (lock-free) olmaları garanti edilir, ancak beklemesiz (wait-free) olmaları garanti edilmez. Her işlem bir Ordering alır; en zayıfı Relaxed yalnızca işlemin dokunduğu belleği senkronize eder, SeqCst ise ek olarak tüm iş parçacıklarının bu işlemleri aynı sırada görmesini sağlar. Sıralamalar C++20 ile aynıdır ve ayrıntıları Rustonomicon'da anlatılır.
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
// Mutex::new ve AtomicUsize::new const fn olduğu için static içinde kullanılabilir.
static ISTEK: AtomicUsize = AtomicUsize::new(0);
static GUNLUK: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn main() {
thread::scope(|s| {
for no in 0..4 {
s.spawn(move || {
for _ in 0..1000 {
ISTEK.fetch_add(1, Ordering::Relaxed);
}
GUNLUK.lock().unwrap().push(format!("işçi {no} bitti"));
});
}
});
println!("istek sayısı: {}", ISTEK.load(Ordering::Relaxed));
let mut gunluk = GUNLUK.lock().unwrap();
gunluk.sort();
println!("{gunluk:?}");
}
// çıktı: istek sayısı: 4000
// çıktı: ["işçi 0 bitti", "işçi 1 bitti", "işçi 2 bitti", "işçi 3 bitti"]Örnekte sayaç için Relaxed yeterli, çünkü değeri yalnızca tüm iş parçacıkları join edildikten sonra okuyoruz ve join belgeye göre iş parçacığının yaptığı her şeyin, join sonrası işlemlerden önce gerçekleşmesini sağlıyor. Başka verilerin görünürlüğü atomik bir değere bağlıysa (örneğin "veri hazır" bayrağı) Release ile yazıp Acquire ile okumanız gerekir. Mutex::new ve atomiklerin new fonksiyonları const fn olduğu için örnekteki gibi doğrudan static içinde kullanılabilir; static mut ve unsafe gerekmez. Bir kez hesaplanacak global değerler için OnceLock ve ilk erişimde başlatılan LazyLock da aynı modüldedir. Atomiklerin bir bedeli vardır: Arc belgesi de atomik işlemlerin sıradan bellek erişimlerinden pahalı olduğunu not eder.
Zehirlenme (poisoning): kilit tutarken panik
Bir iş parçacığı mutex'i kilitliyken panik yaparsa mutex zehirlenir. Bunun amacı, yarıda kalmış bir güncelleme yüzünden tutarsız hale gelmiş olabilecek verinin fark edilmeden kullanılmasını önlemektir. Zehirlenmiş bir mutex'te lock ve try_lock hata döndürür. Çoğu kod bu sonucu unwrap ederek paniğin iş parçacıkları arasında yayılmasını tercih eder; Kitap da örneklerinde bu yolu izler.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let bakiye = Arc::new(Mutex::new(100_i32));
let b2 = Arc::clone(&bakiye);
let sonuc = thread::spawn(move || {
let mut kilit = b2.lock().unwrap();
*kilit -= 30;
panic!("işlem yarıda kaldı"); // kilit tutulurken panik: mutex zehirlenir
})
.join();
println!("iş parçacığı hata ile bitti: {}", sonuc.is_err());
println!("zehirli mi: {}", bakiye.is_poisoned());
// lock artık Err döndürür; veriye yine de into_inner ile ulaşılabilir.
let deger = match bakiye.lock() {
Ok(g) => *g,
Err(zehir) => {
let g = zehir.into_inner();
println!("zehirli kilitten okunan: {}", *g);
*g
}
};
bakiye.clear_poison();
println!("temizlendi, zehirli mi: {}, değer: {deger}", bakiye.is_poisoned());
}
// çıktı: iş parçacığı hata ile bitti: true
// çıktı: zehirli mi: true
// çıktı: zehirli kilitten okunan: 70
// çıktı: temizlendi, zehirli mi: false, değer: 70Belge, zehirlenmenin yalnızca tavsiye niteliğinde olduğunu vurgular: PoisonError::into_inner normalde dönecek guard'ı verir ve veriye yine de ulaşabilirsiniz. Veriyi bilinen iyi bir değerle değiştirdiyseniz ya da tutarlı olduğunu doğruladıysanız clear_poison ile işareti kaldırabilirsiniz. Örnekte bakiye 30 azaltılmış ama işlem tamamlanmadan panik olmuş; gerçek bir sistemde tam da bu yüzden zehirli veriyi körü körüne kullanmamanız gerekir. Belgeye göre panik tespiti kusursuz değildir, bu nedenle unsafe kod güvenliği zehirlenmeye dayandıramaz.
Send ve Sync: derleyicinin iş parçacığı güvenliği kontrolü

Buraya kadar gördüğünüz hataların hepsi iki işaretçi (marker) trait'e dayanır. Send, bir tipin sahipliğinin başka bir iş parçacığına aktarılabileceğini belirtir. Sync ise bir tipe birden çok iş parçacığından referansla erişmenin güvenli olduğunu söyler; Kitabın tanımıyla &T değeri Send ise T değeri Sync olur. Tamamen Send parçalardan oluşan bir tip otomatik olarak Send, tamamen Sync parçalardan oluşan tip de otomatik olarak Sync olur. Bu trait'lerin metodu yoktur ve elle uygulamak unsafe gerektirir; bunun için Rust unsafe yazısına bakın.
| Tek iş parçacığı | Çok iş parçacığı | Not |
|---|---|---|
| Rc<T> | Arc<T> | Rc ne Send ne Sync; Arc, T Send ve Sync ise ikisini de uygular |
| RefCell<T> | Mutex<T> veya RwLock<T> | RefCell ve Cell Sync değildir, Mutex Sync uygular |
| Cell<u32> | AtomicU32 | Basit sayı ve bayraklar için kilitsiz seçenek |
| Ham işaretçiler | Yok | Kitaba göre ham işaretçiler Send değildir |
Sık yapılan hatalar
- Kilidi gereğinden uzun tutmak: guard'ı uzun bir fonksiyonun başında alıp sonuna kadar tutmak paralelliği yok eder. Kritik bölgeyi küçük bir bloğa alın.
- Aynı mutex'i aynı iş parçacığında iki kez kilitlemek: davranış tanımsız bırakılmıştır; panik ya da kilitlenme yaşarsınız.
- Kilitleri farklı sırayla almak: iki iş parçacığı A ve B kilitlerini ters sırayla alırsa birbirini bekleyerek kilitlenir.
- Arc'ın veriyi koruduğunu sanmak:
Arc<T>sadece paylaşılan sahiplik verir; değiştirme için içeride bir Mutex, RwLock ya da atomik gerekir. - Basit sayaç için Mutex kullanmak: çalışır ama gereğinden ağırdır; atomik tipler bu iş için tasarlanmıştır.
- lock().unwrap() sonucunu hiç düşünmemek: zehirlenmiş mutex'te panik alırsınız. Bu çoğu zaman doğru davranıştır, ama nedenini bilerek seçin.
Sık Sorulan Sorular
Mutex mi kanal mı kullanmalıyım?
Veri bir iş parçacığından diğerine akıyorsa kanal, birçok iş parçacığı aynı yapıyı okuyup güncelliyorsa Mutex ya da RwLock daha doğaldır. Kitap ikisini sırasıyla tek sahiplik ve çoklu sahiplik modellerine benzetir. Kanallar için Rust mpsc kanal yazısına bakın.
Mutex<T> neden mut olmadan değiştirilebiliyor?
Mutex iç değişebilirlik (interior mutability) sağlar. lock metodu &self alır ve tekil erişimi derleme anında değil, çalışma anında kilitle garanti eder. Bu, tek iş parçacıklı dünyadaki RefCell ile aynı fikirdir.
Arc::clone ile x.clone() arasında fark var mı?
Davranış aynıdır; belge iki yazımı eşdeğer sayar. Arc::clone(&x) yazımı, okuyana derin bir kopya değil yalnızca referans sayısı artışı yapıldığını açıkça gösterdiği için Kitapta tercih edilir.
Async kodda std Mutex kullanabilir miyim?
lock çağıran iş parçacığını bloklar; guard'ı bir .await noktası boyunca tutmak yürütücüyü durdurabilir. Async çalışma zamanları kendi kilit tiplerini sunar ve bunlar std kapsamı dışındadır. Async temelleri için Rust async/await yazısına bakın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Rust öğrenme rehberi · Rust thread::spawn · Rust mpsc kanalları · Rust Rc ve RefCell · Rust E0277 hatası · Rust unsafe