Tüm Rust rehberleri
TEKNİK REHBER EŞZAMANLILIK THREAD 2026

Eşzamanlılık
Korkusuz paralellik

Thread'ler, channel'lar, Mutex/Arc ve Send/Sync trait'leri; derleme zamanında veri yarışı (data race) güvenliği — C pthread ve manuel kilitlemeyle karşılaştırmalı.

00 Korkusuz eşzamanlılık

C'de paylaşımlı belleğe iki thread'in kilitsiz dokunması bir data race'tir ve standart bunu tanımsız davranış (UB) ilan eder — derleyici uyarmaz, sanitizer çoğu zaman yakalamaz, hata üretimde rastgele patlar. Rust'ta aynı kod derlenmez. Veri yarışı bir runtime sorunu değil, bir tip hatasıdır.

pthreads ile çalışırken bildiğiniz disiplin şudur: hangi mutex'in hangi veriyi koruduğunu kafanızda tutarsınız, her erişimden önce kilitlersiniz, kilit sırasına dikkat edip deadlock'tan kaçınırsınız. Bu sözleşmenin tamamı belgelenmemiş, derleyici tarafından denetlenmeyen bir konvansiyondur. Tek bir unutulmuş pthread_mutex_lock sessizce UB üretir.

Rust aynı garantileri sahip olduğu ownership sisteminden bedavaya çıkarır. Bir değere aynı anda ya bir tane &mut (yazıcı) ya da çok sayıda & (okuyucu) erişebilir — bu kural tek thread'de aliasing hatalarını önler; çoklu thread'de ise data race'in tanımının kendisini ortadan kaldırır, çünkü data race = "en az biri yazan, eşzamanlı, senkronizasyonsuz iki erişim".

C/pthread sözleşmesi          Rust'ta zorunlu kıldıran
─────────────────             ─────────────────
"kilitlemeyi unutma"     →     Mutex erişimi lock() ister
"kilit + veri eşleşsin"  →     veri Mutex'in İÇİNDE
"bu tip thread-safe mi?" →     Send / Sync trait'leri
"dangling thread yok"    →     'static + JoinHandle

İki dik mekanizma

Rust eşzamanlılığı iki bağımsız ekseni denetler. Birincisi borrow checker: bir veriye kaç referansın hangi türden eriştiğini izler. İkincisi Send/Sync auto trait'leri: bir tipin thread sınırlarını geçip geçemeyeceğini tip düzeyinde işaretler. std::thread::spawn, kapatma (closure) içine taşınan her şeyin Send + 'static olmasını isteyen tek bir imzayla bütün modeli devreye sokar.

NOT

"Korkusuz eşzamanlılık" (fearless concurrency) pazarlama değil, ölçülebilir bir iddia: yanlış senkronizasyon kategorisindeki hataların büyük kısmı derleme aşamasında durur. Geriye kalan asıl risk deadlock'tur — Rust onu engellemez, çünkü deadlock bir bellek güvenliği ihlali değildir.

Bu bölümde

  • C'de data race UB'dir ve sessizdir; Rust'ta aynı kod derlenmez
  • Borrow checker'ın aliasing kuralı doğrudan data race'i tanımsız kılar
  • İki dik eksen: ownership/borrow ve Send/Sync auto trait'leri
  • Deadlock derlemeyle engellenmez — o bir mantık hatasıdır, bellek hatası değil

01 thread::spawn ve join

thread::spawn, pthread_create'in yerini tutar ama imzası taban tabana farklı: void* alıp void* döndüren bir C fonksiyonu yerine, doğrudan tipli bir closure alır ve geriye JoinHandle<T> verir — pthread'in void** retval kanalını tip güvenli hale getirir.

Closure'a dışarıdan veri taşımak için move anahtar kelimesi kullanılır: bu, yakalanan değerlerin sahipliğini yeni thread'e geçirir. C'de argümanı void* olarak paketleyip thread'in ömrü boyunca o belleğin yaşadığından elle emin olmanız gerekirdi; Rust'ta move sahipliği devrettiği için derleyici bunu garanti eder.

main.rs
use std::thread;

fn main() {
    let mesaj = String::from("merhaba");

    // move: `mesaj`in sahipliği thread'e geçer (pthread'de void* arg)
    let tutamac = thread::spawn(move || {
        println!("alt thread: {}", mesaj);
        mesaj.len() // dönüş değeri JoinHandle<usize> ile taşınır
    });

    // join() == pthread_join; retval tip güvenli gelir
    let uzunluk = tutamac.join().unwrap();
    println!("thread {} bayt yazdı", uzunluk);
}
spawn(f)FnOnce + Send + 'static bir closure alır, JoinHandle<T> döndürür.
JoinHandle<T>Thread'in tutamacı; T closure'ın dönüş tipidir.
.join()Thread'i bekler, Result<T, Box<dyn Any>> verir; Err = panic.
DİKKAT

JoinHandle üzerinde join() çağırmazsanız thread detach edilmiş olur — main biterse process'le birlikte aniden ölür. C'deki gibi "sessizce arkada koşmaya devam etmez"; bekleneni açıkça join etmelisiniz.

Thread'in panik atması process'i çökertmez: panic o thread'le sınırlı kalır ve join() bir Err olarak geri döner. Bu, pthread'de bir thread'in segfault ile tüm process'i indirmesinden temelde farklıdır.

Bu bölümde

  • spawn tipli closure alır; void* paketleme yok
  • move yakalanan verinin sahipliğini thread'e devreder
  • JoinHandle<T> dönüş değerini tip güvenli taşır; join() bekler
  • Join edilmeyen thread detach olur; panik o thread'le sınırlı kalır

02 Thread'lerde borrow checker

C'de yığın (stack) üzerindeki bir değişkenin adresini bir thread'e verip ana fonksiyonun erken dönmesi — klasik dangling pointer. Rust bu senaryoyu spawn'ın 'static bound'u ile derleme zamanında reddeder: spawn edilen closure, oluşturan stack frame'den uzun yaşayabilmek zorundadır.

spawn imzası, taşınan closure'ın 'static ömre sahip olmasını ister. Bunun anlamı: closure ödünç alınmış (borrowed) hiçbir referansa bağımlı olamaz; ya sahiplenmiş veri taşır ya da gerçekten 'static olan (örn. statik) bir şeye dokunur. Çıplak bir &veri yakalamak derlenmez.

main.rs
use std::thread;

fn main() {
    let dizi = vec![1, 2, 3];

    // HATA: closure `&dizi` ödünç alır, ama thread `dizi`den
    // uzun yaşayabilir → derlenmez (E0373/'static ihlali)
    let h = thread::spawn(|| {
        println!("{:?}", dizi); // borrow
    });

    h.join().unwrap();
}

Derleyicinin önerdiği çözüm tektir: move ile sahipliği taşıyın. Tek bir thread veriyi tüketecekse bu yeterli. Birden fazla thread aynı veriye erişecekse, sahipliği paylaşmak gerekir — bu da Bölüm 05'teki Arc'ın varlık sebebidir.

main.rs
use std::thread;

fn main() {
    let dizi = vec![1, 2, 3];

    // move: `dizi`nin sahipliği thread'e geçer; artık borrow yok
    let h = thread::spawn(move || {
        println!("{:?}", dizi);
    });
    // burada `dizi` artık kullanılamaz — sahiplik gitti

    h.join().unwrap();
}
NOT

'static burada "programın tüm ömrü kadar yaşar" demek değildir; "ömrü ödünç alınmış bir referansa bağlı değildir" demektir. Sahiplenilmiş bir String ya da Vec mükemmel biçimde 'static'tir, çünkü içsel hiçbir borrow taşımaz.

Bu bölümde

  • spawn closure'ın 'static olmasını ister; çıplak referans yakalanamaz
  • Tek thread tüketecekse move ile sahipliği taşımak yeterli
  • Çok thread aynı veriye erişecekse paylaşımlı sahiplik (Arc) gerekir
  • 'static = "borrow'a bağlı değil", "sonsuza dek yaşar" değil

03 Channel (mpsc)

Paylaşımlı belleği kilitle korumanın alternatifi mesajlaşmadır: "belleği paylaşarak iletişme; ileterek belleği paylaş." std::sync::mpsc, Go'nun kanallarına benzer ama tek farkla — Rust kanalında geçen değerin sahipliği alıcıya transfer olur, kopya değil paylaşım değil, taşıma.

mpsc = multiple producer, single consumer. channel() bir (Sender<T>, Receiver<T>) çifti üretir. Gönderen tarafta send, alan tarafta recv (bloklayan) ya da for v in rx (kanal kapanana dek iterasyon) kullanılır.

main.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<String>();

    thread::spawn(move || {
        let s = String::from("iş sonucu");
        tx.send(s).unwrap(); // `s`in SAHİPLİĞİ kanala geçti
        // `s` burada artık kullanılamaz — use-after-send imkânsız
    });

    // recv() bloklar; kanal kapanırsa Err döner
    let alinan = rx.recv().unwrap();
    println!("ana thread aldı: {}", alinan);
}

Sahipliğin transferi, C'de paylaşımlı kuyruklarda görülen en sinsi hatayı kökten yok eder: bir thread bir nesneyi kuyruğa koyup aynı pointer'ı kullanmaya devam etmesi. Rust'ta send'den sonra değer artık gönderende yoktur; data race veya use-after-free yazmak mümkün değildir.

Çoklu üretici

Sender klonlanabilir; her klon aynı tek alıcıya yazar. C'de tek bir kuyruğu N thread'in beslemesi için kuyruğun etrafına manuel mutex sararsınız — burada clone() yeterli, senkronizasyon kanalın içindedir.

main.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    for id in 0..3 {
        let tx = tx.clone(); // her üretici kendi Sender'ı
        thread::spawn(move || {
            tx.send(format!("worker {id}")).unwrap();
        });
    }
    drop(tx); // orijinali bırak; yoksa kanal hiç kapanmaz

    // kanal kapanana (tüm Sender düşene) dek iterasyon
    for m in rx {
        println!("alındı: {}", m);
    }
}
DİKKAT

Klonladıktan sonra orijinal tx'i drop etmeyi unutursanız, for m in rx döngüsü asla bitmez: kanal yalnızca tüm Sender'lar düştüğünde kapanır. Bu, Go'da close(ch)'i unutmaya denk bir tuzaktır.

Bu bölümde

  • Kanal = sahiplik transferiyle mesajlaşma; kopya/paylaşım değil taşıma
  • send sonrası değer gönderende kalmaz → use-after-send imkânsız
  • Sender klonlanarak çoklu üretici (mpsc) kurulur
  • Kanal tüm Sender'lar düşünce kapanır; orijinali drop etmeyi unutma

04 Mutex<T>

C'de mutex ile koruduğu veri iki ayrı değişkendir; aralarındaki bağ yalnızca programcının zihnindedir. Rust'ta Mutex<T> veriyi içine alır — korunan değere ancak kilidi alarak ulaşabilirsiniz, dolayısıyla "kilitlemeyi unutmak" tip düzeyinde imkânsızlaşır.

lock() çağrısı bir MutexGuard<T> döndürür. Bu guard, &mut T gibi davranan bir akıllı işaretçidir (Deref + DerefMut); kapsam dışına çıktığında Drop ile kilidi otomatik bırakır. pthread_mutex_unlock'u elle çağırmazsınız — RAII bunu garantiler, erken return ya da panic'te bile.

main.rs
use std::sync::Mutex;

fn main() {
    // Veri kilidin İÇİNDE — ayrı "kilit + veri" hatası yok
    let sayac = Mutex::new(0);

    {
        // lock() == pthread_mutex_lock; guard kilidi temsil eder
        let mut g = sayac.lock().unwrap();
        *g += 1; // MutexGuard, &mut T gibi davranır
    } // g burada drop olur → kilit otomatik bırakılır (unlock)

    println!("sayac = {}", *sayac.lock().unwrap());
}

Poisoning

Bir thread kilidi tutarken panik atarsa, Mutex poisoned (zehirli) duruma geçer: korunan veri yarım kalmış olabilir. Sonraki lock() çağrıları bunu bir Err ile bildirir; bu yüzden dönüş tipi Result'tır. Sıkça görülen .unwrap() "zehirlenmişse panikle çök" anlamına gelir; gerçek kodda into_inner() ile veriye yine de erişmeyi seçebilirsiniz.

NOT

Poisoning C'de yoktur; bir thread kilit altında çökerse C'de mutex bozuk-tutarlı veriyle "açık" kalır ve diğer thread'ler farkında olmadan o veriyi okur. Rust en azından bu durumu Err olarak görünür kılar.

DİKKAT

Guard'ı bir değişkene bağlayıp kapsam içinde tutarsanız kilit beklediğinizden uzun süre elde kalır. Kilidin yaşam süresini daraltmak için { ... } bloğu ya da bilinçli bir drop(g) kullanın — aksi halde gizli darboğaz ve deadlock riski.

Bu bölümde

  • Mutex<T> veriyi içine alır; kilitsiz erişim tipçe imkânsız
  • lock() bir MutexGuard verir; drop'ta otomatik unlock (RAII)
  • Panik altında poisoning kilidi zehirler; lock() bu yüzden Result
  • Guard ömrünü blok/drop ile daralt — uzun tutarsa darboğaz

05 Arc<Mutex<T>>

Tek bir Mutex'i N thread paylaşmak için ona N tane sahip lazım — ama Rust'ta bir değerin tek bir sahibi olur. Çözüm Arc (atomically reference counted): paylaşımlı sahiplik. Thread'ler arası değiştirilebilir paylaşımlı durumun kanonik kalıbı Arc<Mutex<T>>'tir.

Roller nettir: Arc "bu veriye birden çok sahip var" der ve sayım sıfırlanınca veriyi serbest bırakır; Mutex ise "aynı anda yalnız biri yazsın" der. Birlikte: çok sahipli + güvenli değiştirilebilir. Klasik örnek, on thread'in tek bir sayacı artırması.

main.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let sayac = Arc::new(Mutex::new(0));
    let mut tutamaclar = vec![];

    for _ in 0..10 {
        let sayac = Arc::clone(&sayac); // refcount++; veri kopyalanmaz
        tutamaclar.push(thread::spawn(move || {
            let mut n = sayac.lock().unwrap();
            *n += 1; // kilit altında güvenli artış
        }));
    }

    for h in tutamaclar { h.join().unwrap(); }
    println!("toplam = {}", *sayac.lock().unwrap()); // 10
}
Arc::new(x)Heap'te paylaşımlı sahiplik; atomik refcount başlatır.
Arc::clone(&a)Sayacı artırır, veriyi kopyalamaz; ucuz bir pointer paylaşımı.
Mutex::new(x)Korunan veriyi sarmalar; lock() ile erişilir.

Neden Rc değil Arc

Rc de paylaşımlı sahiplik verir ama refcount'u atomik değildir — tek thread için tasarlanmıştır, daha hızlıdır. İki thread aynı Rc'yi klonlasa sayaçta data race olurdu. İşte bu yüzden Rc !Send'dir: derleyici onu bir thread'e taşımanıza izin vermez. Arc atomik sayaç kullanır, dolayısıyla Send + Sync'tir. Yani "yanlış sayaç" seçimi derleme hatasıdır, runtime sürprizi değil.

NOT

Atomik sayaç ucuz değildir; tek thread'de Rc daima tercih edilir. Arc'ı yalnızca veri gerçekten thread sınırını geçecekse kullanın — derleyici zaten Rc yetersizse uyaracaktır.

Bu bölümde

  • Arc = paylaşımlı sahiplik; Mutex = güvenli değiştirme
  • Arc::clone sayacı artırır, veriyi kopyalamaz (ucuz)
  • Rc atomik olmayan sayaç yüzünden !Send'dir; thread'e geçemez
  • Arc<Mutex<T>> paylaşımlı değiştirilebilir durumun kanonik kalıbı

06 Send ve Sync

Bir tipin thread'lerle nasıl etkileşebileceğini iki auto trait kodlar. Send: tip bir başka thread'e taşınabilir. Sync: &T thread'ler arası paylaşılabilir (eşdeğer olarak: &T Send'dir). C'de "bu struct thread-safe mi?" sorusunun yanıtı belgelemededir; Rust'ta tip sisteminin içindedir.

Bu trait'ler "auto"dur: bileşenlerinin hepsi Send/Sync ise tip otomatik olarak Send/Sync sayılır. Hiçbir şey elle implemente etmezsiniz; derleyici tip ağacını gezerek türetir. Bir tipi kasıtlı olarak unsafe yollarla işaretlemediğiniz sürece kurallar mekaniktir.

TipSendSyncNeden
Arc<T>evet*evet*atomik refcount (T: Send+Sync ise)
Rc<T>hayırhayıratomik olmayan sayaç → data race
Mutex<T>evet*evet*kilit erişimi serileştirir
Cell/RefCellevethayırruntime borrow paylaşımda yarışır
çıplak işaretçi *mut Thayırhayırgüvenlik garantisi yok

Anahtar nokta: thread::spawn'ın bound'u F: Send + 'static'tir. Yani bir thread'e Send olmayan bir şey taşımaya kalkarsanız hata kullanım yerinde, açık bir mesajla çıkar. Rc örneğindeki "neden derlenmedi?" sorusunun tek cümlelik yanıtı: Rc !Send'dir.

main.rs
use std::rc::Rc;
use std::thread;

fn main() {
    let r = Rc::new(5);

    // HATA: `Rc<i32>` Send değil → thread'e taşınamaz
    // error[E0277]: `Rc<i32>` cannot be sent between threads safely
    thread::spawn(move || {
        println!("{}", r);
    });
}
NOT

Send ve Sync ortogonaldir. MutexGuard ilginç bir örnektir: Sync'tir (referansı paylaşılabilir) ama bazı platformlarda !Send'dir — yani guard'ı kilitleyen thread bırakmalıdır. Çoğu tipte ikisi birlikte gelir; ayrımı yalnızca bu tür sınır durumlarda hissedersiniz.

Bu bölümde

  • Send = thread'e taşınabilir; Sync = &T paylaşılabilir
  • Auto trait: bileşenler uygunsa derleyici otomatik türetir
  • Rc !Send (atomik olmayan sayaç); Arc Send + Sync
  • spawn'ın Send + 'static bound'u kuralı kullanım yerinde zorlar

07 RwLock ve atomics

Tek bir Mutex okuyucuları da serileştirir; oysa çoğu okuyucu / az yazıcı yükünde bu israftır. RwLock<T> tam olarak pthread_rwlock_t'nin karşılığıdır: aynı anda çok okuyucu ya da tek yazıcı. Daha da hafifi gerekiyorsa, tek skaler değerler için atomics kilidi tamamen ortadan kaldırır.

RwLock iki ayrı kilitleme verir: read() paylaşımlı bir guard (&T), write() dışlayıcı bir guard (&mut T) döndürür. Borrow checker'ın "çok okuyucu veya tek yazıcı" kuralının çalışma zamanına taşınmış halidir.

main.rs
use std::sync::RwLock;

fn main() {
    let ayar = RwLock::new(vec![1, 2, 3]);

    {
        let r1 = ayar.read().unwrap(); // paylaşımlı; eşzamanlı r2 olabilir
        let r2 = ayar.read().unwrap();
        println!("{} {}", r1.len(), r2.len());
    } // okuyucular drop olur

    {
        let mut w = ayar.write().unwrap(); // dışlayıcı
        w.push(4);
    }
}

Atomics

Korunan veri tek bir tamsayı ya da bayrak ise kilit aşırıdır. AtomicUsize, AtomicBool gibi tipler donanım atomik komutlarıyla çalışır — kilitsiz (lock-free). C11 <stdatomic.h> ya da GCC __atomic builtin'lerini biliyorsanız model birebir aynıdır; Ordering da C++/C bellek sıralamasının (memory order) aynısıdır.

main.rs
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let sayac = Arc::new(AtomicUsize::new(0));
    let mut hs = vec![];

    for _ in 0..10 {
        let c = Arc::clone(&sayac);
        hs.push(thread::spawn(move || {
            // kilitsiz artış; basit sayaç için Relaxed yeter
            c.fetch_add(1, Ordering::Relaxed);
        }));
    }
    for h in hs { h.join().unwrap(); }
    println!("{}", sayac.load(Ordering::Relaxed)); // 10
}
DİKKAT

Ordering seçimi ciddidir. Relaxed yalnızca atomikliği garanti eder, başka veriyle sıralama kurmaz — bağımsız bir sayaç için doğru ama bir bayrakla başka belleği "yayınlıyorsanız" Release/Acquire gerekir. Şüphedeyseniz SeqCst kullanın; C/C++'taki kurallarla aynıdır.

Bu bölümde

  • RwLock = çok okuyucu / tek yazıcı; pthread_rwlock karşılığı
  • read() paylaşımlı, write() dışlayıcı guard verir
  • Tek skaler için Atomic* kilitsizdir; C11/C++ atomic modeliyle aynı
  • Ordering C/C++ memory order'ıyla birebir; şüphede SeqCst

08 Scoped threads

spawn'ın 'static bound'u, sadece kısa süre çalışıp ana fonksiyonun yerel verisini ödünç almak isteyen thread'ler için fazla katıdır. std::thread::scope (Rust 1.63+) tam bu boşluğu doldurur: scope bitmeden tüm child thread'lerin join'ini garanti ettiği için, ödünç alınan veriye Arc'sız, klonsuz erişebilirsiniz.

Mekanizma şu: scope kapanmadan önce içinde açılan her thread'i otomatik join eder. Derleyici bunu bildiği için, child thread'lerin yerel bir &data'yı yakalamasına izin verir — çünkü o veri, thread'ler bitmeden asla yok olamaz. 'static şartı bu kapsamda gevşer.

main.rs
use std::thread;

fn main() {
    let dizi = vec![1, 2, 3, 4, 5, 6];

    thread::scope(|s| {
        // her iki thread `dizi`yi PAYLAŞIMLI ödünç alır — Arc yok
        s.spawn(|| {
            let toplam: i32 = dizi[..3].iter().sum();
            println!("ilk yarı: {toplam}");
        });
        s.spawn(|| {
            let toplam: i32 = dizi[3..].iter().sum();
            println!("ikinci yarı: {toplam}");
        });
    }); // scope kapanır → her iki thread otomatik join edilir

    // `dizi` hâlâ geçerli; sahipliği hiç taşınmadı
    println!("toplam eleman: {}", dizi.len());
}
NOT

Scope içinde de &mut kuralları geçerli: aynı veriyi iki thread paylaşımlı (&) okuyabilir, ama ikisi birden değiştiremez. Eşzamanlı yazma gerekiyorsa scope içinde bile Mutex ya da disjoint (örtüşmeyen) dilimler kullanmanız gerekir.

Ne zaman pratik? Veriyi yaşatan stack frame thread'lerden uzun yaşayacaksa ve iş kısa süreliyse scoped threads en temiz seçimdir: Arc/klon gürültüsü ve atomik refcount maliyeti olmadan paralellik. Uzun ömürlü, ana fonksiyondan bağımsız çalışan thread'ler için ise hâlâ klasik spawn + Arc gerekir.

Bu bölümde

  • thread::scope child thread'leri scope sonunda otomatik join eder
  • Bu garanti sayesinde yerel veri Arc'sız ödünç verilebilir
  • Paylaşımlı borrow serbest; eşzamanlı yazma için yine Mutex/disjoint
  • Kısa ömürlü, yerel veriye dokunan işler için en temiz kalıp

09 Gerçek örnek

Şimdi parçaları birleştirelim: sabit sayıda worker thread'in bir kanaldan iş çekip, sonucu paylaşımlı bir Arc<Mutex<T>>'te biriktirdiği klasik bir desen. Bu, C'de "thread pool + iş kuyruğu + sonuç mutex'i" üçlüsünün doğrudan ama güvenli karşılığıdır.

İşler bir mpsc kanalına gönderilir; N worker aynı Receiver'ı paylaşamayacağı için (mpsc tek tüketicilidir) ReceiverArc<Mutex<...>> ile sararız — her worker kilitleyip bir iş alır. Sonuçlar paylaşımlı bir toplam mutex'inde birikir.

main.rs
use std::sync::{Arc, Mutex, mpsc};
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<u64>();
    // rx paylaşımlı: çok worker tek kuyruktan çeker
    let rx = Arc::new(Mutex::new(rx));
    let toplam = Arc::new(Mutex::new(0u64));

    let mut workerlar = vec![];
    for _ in 0..4 {
        let rx = Arc::clone(&rx);
        let toplam = Arc::clone(&toplam);
        workerlar.push(thread::spawn(move || {
            loop {
                // kilidi kısa tut: yalnız bir iş çek, sonra bırak
                let is = rx.lock().unwrap().recv();
                match is {
                    Ok(n) => {
                        let kare = n * n;       // kilit DIŞINDA hesapla
                        *toplam.lock().unwrap() += kare;
                    }
                    Err(_) => break, // kanal kapandı → çık
                }
            }
        }));
    }

    for i in 1..=100u64 { tx.send(i).unwrap(); }
    drop(tx); // tüm işler gönderildi; kanalı kapat

    for w in workerlar { w.join().unwrap(); }
    println!("kareler toplamı = {}", *toplam.lock().unwrap()); // 338350
}

Dikkat edilen iki nokta üretim kodunda kritiktir: ağır hesaplama (n * n burada simgesel) kilit dışında yapılır, böylece worker'lar darboğaza girmez; ve kanal drop(tx) ile kapatıldığı için recv() bir Err döner ve döngü temiz biter. C'de bu "kapanış sinyali" için ayrı bir bayrak + condition variable kurardınız.

Kapanış: rayon

Yukarıdaki desen heterojen iş kuyrukları için doğru araçtır. Ama yaptığınız tek şey bir koleksiyonu paralel işlemekse (data parallelism), elle thread pool kurmak gereksizdir. rayon crate'i bunu tek satıra indirir: iter() yerine par_iter() yazarsınız, kütüphane work-stealing scheduler ile çekirdeklere böler.

main.rs
use rayon::prelude::*;

fn main() {
    // aynı toplam, sıfır manuel senkronizasyon
    let toplam: u64 = (1..=100u64)
        .into_par_iter()      // paralel iterator
        .map(|n| n * n)
        .sum();
    println!("kareler toplamı = {toplam}"); // 338350
}
NOT

rayon güvenliğini yine Send/Sync'ten alır: par_iter, closure'ınızın Send olmasını ister, yani aynı derleme zamanı veri-yarışı garantisi geçerlidir. Pratik kural: düzenli data parallelism → rayon; özel iş akışı / heterojen görevler → elle channel + Arc<Mutex>.

Bu bölümde

  • Worker pool: Arc<Mutex<Receiver>> ile çok worker tek kuyruktan çeker
  • Ağır işi kilit dışında yap; kilidi sadece veri paylaşımı için kısa tut
  • drop(tx) kanalı kapatır → worker'lar Err görüp temiz biter
  • Düz data parallelism için rayon par_iter elle pool kurmaya yeğdir