Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Paylaşımlı Durum Eşzamanlılığı

Mesaj iletimi, eşzamanlılığı yönetmenin iyi yollarından biridir; ama tek yol bu değildir. Başka bir yöntem de birden fazla iş parçacığının aynı paylaşılan veriye erişmesidir. Go belgelerindeki şu sözün bu bölümünü yeniden düşünün: “Belleği paylaşarak iletişim kurmayın.”

Peki belleği paylaşarak iletişim kurmak nasıl bir şeye benzerdi? Ayrıca mesaj iletimini savunanlar neden bellek paylaşımından özellikle kaçınmayı öneriyor?

Bir bakıma, herhangi bir programlama dilindeki kanallar tek sahipliğe benzer; çünkü bir değeri kanaldan gönderdiğinizde artık onu kullanmamanız gerekir. Paylaşımlı bellekle eşzamanlılık ise çoklu sahipliğe benzer: Birden çok iş parçacığı aynı anda aynı bellek konumuna erişebilir. 15. bölümde akıllı işaretçilerin çoklu sahipliği nasıl mümkün kıldığını görmüştünüz. Çoklu sahiplik, farklı sahiplerin yönetilmesini gerektirdiği için doğal olarak karmaşıklık da getirir. Rust’ın tür sistemi ve sahiplik kuralları bu yönetimi doğru yapma konusunda çok yardımcı olur. Bir örnek olarak, paylaşımlı bellek eşzamanlılığında sık kullanılan temel yapılardan biri olan mutex’e bakalım.

Mutex<T> ile Erişimi Denetlemek

Mutex, mutual exclusion ifadesinin kısaltmasıdır; yani bir mutex, belli bir anda yalnızca tek bir iş parçacığının bazı verilere erişmesine izin verir. Mutex içindeki veriye erişmek için, iş parçacığı önce erişim istediğini bildirmeli ve mutex’in kilidini (lock) almalıdır. Kilit, mutex’in bir parçası olan ve o anda veriye kimin özel erişimi olduğunu takip eden veri yapısıdır. Bu yüzden mutex’in tuttuğu veriyi kilitleme sistemiyle koruduğu söylenir.

Mutex’lerin kullanımı zor olmakla ünlüdür; çünkü iki kuralı sürekli akılda tutmanız gerekir:

  1. Veriyi kullanmadan önce kilidi almaya çalışmalısınız.
  2. Mutex’in koruduğu veriyle işiniz bittiğinde, diğer iş parçacıkları da kilidi alabilsin diye kilidi bırakmalısınız.

Bunu gündelik bir benzetmeyle düşünelim: Tek mikrofonu olan bir panel oturumunu hayal edin. Konuşmacılardan biri konuşmadan önce mikrofonu istediğini belirtmelidir. Mikrofonu alınca istediği kadar konuşur; sonra da sıradaki kişiye verir. Bir konuşmacı işini bitirince mikrofonu devretmeyi unutursa, başka kimse konuşamaz. Paylaşılan mikrofonun yönetimi bozulursa panel planlandığı gibi ilerlemez.

Mutex yönetimini doğru yapmak gerçekten zordur; işte bu yüzden birçok kişi kanalları daha heyecan verici bulur. Ama Rust’ın tür sistemi ve sahiplik kuralları sayesinde, kilitleme ile kilidi bırakma işlerini yanlış yapmanız çok daha zordur.

Mutex<T> API’si

Mutex’in nasıl kullanıldığını görmek için önce tek iş parçacıklı çok basit bir örnekle başlayalım; 16-12 numaralı liste bunu gösteriyor.

Filename: src/main.rs
use std::sync::Mutex;

fn main() {
    let kilit = Mutex::new(5);

    {
        let mut sayi = kilit.lock().unwrap();
        *sayi = 6;
    }

    println!("kilit = {kilit:?}");
}
Listing 16-12: Sade olması için Mutex<T> API’sini tek iş parçacıklı bağlamda incelemek

Birçok türde olduğu gibi Mutex<T> değerini de new ilişkili fonksiyonuyla oluştururuz. İçindeki veriye erişmek için lock metoduyla kilidi alırız. Bu çağrı, kilidi alma sırası bize gelene kadar mevcut iş parçacığını bloklar.

Kilidi elinde tutan başka bir iş parçacığı paniklerse lock çağrısı hata verebilir. Böyle bir durumda artık hiç kimse kilidi alamayacağı için, burada unwrap kullanıp bu iş parçacığının da paniklemesini seçtik.

Kilidi aldıktan sonra, burada sayi adını verdiğimiz dönüş değerini içerideki veriye ait değiştirilebilir referans gibi kullanabiliriz. Tür sistemi, kilit değerinin içindeki veriyi kullanmadan önce gerçekten kilit aldığımızdan emin olur. kilit değişkeninin türü i32 değil Mutex<i32> olduğu için, içerideki i32 değeri kullanabilmek adına zorunlu olarak lock çağırırız. Bunu unutamayız; tür sistemi başka türlü izin vermez.

lock çağrısı, bizim unwrap ile ele aldığımız LockResult içine sarılmış bir MutexGuard döndürür. MutexGuard, Deref uygular; böylece içerideki veriyi işaret eder. Ayrıca Drop uygulaması sayesinde bir MutexGuard kapsam dışına çıktığında kilit otomatik olarak bırakılır. Bu da içteki kapsamın sonunda olur. Sonuç olarak kilidi bırakmayı unutup mutex’i başka iş parçacıklarının kullanmasına engel olma riski yaşamayız; kilit bırakma işi kendiliğinden olur.

Kilidi bıraktıktan sonra mutex değerini ekrana yazdırabilir ve içerideki i32 değerini 6 yaptığımızı görebiliriz.

Mutex<T> İçin Paylaşımlı Erişim

Şimdi Mutex<T> kullanarak bir değeri birden fazla iş parçacığı arasında paylaştırmayı deneyelim. 10 iş parçacığı oluşturup her birinin sayacı 1 artırmasını isteyeceğiz; böylece sayaç 0’dan 10’a çıkacak. 16-13 numaralı listedeki örnek derleyici hatası verecek ve bu hata üzerinden Mutex<T> ile çalışırken Rust’ın bize nasıl yardım ettiğini daha iyi anlayacağız.

Filename: src/main.rs
use std::sync::Mutex;
use std::thread;

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

    for _ in 0..10 {
        let tutamac = thread::spawn(move || {
            let mut sayi = sayac.lock().unwrap();

            *sayi += 1;
        });
        tutamaclar.push(tutamac);
    }

    for tutamac in tutamaclar {
        tutamac.join().unwrap();
    }

    println!("Sonuç: {}", *sayac.lock().unwrap());
}
Listing 16-13: Her biri Mutex<T> ile korunan sayacı artıran on iş parçacığı

16-12 numaralı listedekine benzer şekilde, Mutex<T> içindeki bir i32 değerini tutmak için sayac değişkeni oluşturuyoruz. Ardından bir sayı aralığı üzerinde dönerek 10 iş parçacığı başlatıyoruz. thread::spawn çağrısına, sayacı iş parçacığına taşıyan, lock metoduyla Mutex<T> kilidini alan ve sonra içerdeki değere 1 ekleyen aynı kapanışı veriyoruz. Bir iş parçacığı kapanışı bitirdiğinde sayi kapsam dışına çıkar ve kilit bırakılır; böylece başka bir iş parçacığı kilidi alabilir.

Ana iş parçacığında bütün tutamaçları bir vektörde topluyoruz. Sonra 16-2 numaralı listedeki gibi her tutamaç üzerinde join çağırarak tüm iş parçacıklarının bitmesini bekliyoruz. En sonunda ana iş parçacığı kilidi alıp programın sonucunu yazdırıyor.

Bu örneğin derlenmeyeceğini söylemiştik. Şimdi nedenine bakalım:

$ cargo run
   Compiling paylasimli-durum v0.1.0 (file:///projects/paylasimli-durum)
error[E0382]: borrow of moved value: `sayac`
  --> src/main.rs:21:29
   |
 5 |     let sayac = Mutex::new(0);
   |         ----- move occurs because `sayac` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
 8 |     for _ in 0..10 {
   |     -------------- inside of this loop
 9 |         let tutamac = thread::spawn(move || {
   |                                     ------- value moved into closure here, in previous iteration of loop
...
21 |     println!("Sonuç: {}", *sayac.lock().unwrap());
   |                            ^^^^^ value borrowed here after move
   |
help: consider moving the expression out of the loop so it is only moved once
   |
 8 ~     let mut value = sayac.lock();
 9 ~     for _ in 0..10 {
10 |         let handle = thread::spawn(move || {
11 ~             let mut num = value.unwrap();
   |

For more information about this error, try `rustc --explain E0382`.
error: could not compile `paylasimli-durum` (bin "paylasimli-durum") due to 1 previous error

Hata mesajı sayac değerinin döngünün önceki yinelemesinde taşındığını söylüyor. Yani Rust bize, kilitli sayac değerinin sahipliğini birden fazla iş parçacığına taşıyamayacağımızı anlatıyor. Bunu, 15. bölümde gördüğümüz çoklu sahiplik yaklaşımıyla düzeltmeye çalışalım.

Birden Fazla İş Parçacığıyla Çoklu Sahiplik

  1. bölümde bir değere birden fazla sahip vermek için Rc<T> akıllı işaretçisini kullanmıştık. Aynı şeyi burada da yapıp ne olacağına bakalım. 16-14 numaralı listede Mutex<T> değerini Rc<T> içine sarıyor ve iş parçacığına taşımadan önce Rc<T> değerini klonluyoruz.
Filename: src/main.rs
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

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

    for _ in 0..10 {
        let sayac = Rc::clone(&sayac);
        let tutamac = thread::spawn(move || {
            let mut sayi = sayac.lock().unwrap();

            *sayi += 1;
        });
        tutamaclar.push(tutamac);
    }

    for tutamac in tutamaclar {
        tutamac.join().unwrap();
    }

    println!("Sonuç: {}", *sayac.lock().unwrap());
}
Listing 16-14: Birden fazla iş parçacığının Mutex<T> değerine sahip olabilmesi için Rc<T> kullanmaya çalışmak

Yine derliyoruz ve… bu kez farklı hatalar alıyoruz. Derleyici bize çok şey öğretiyor:

$ cargo run
   Compiling paylasimli-durum v0.1.0 (file:///projects/paylasimli-durum)
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
  --> src/main.rs:11:36
   |
11 |           let handle = thread::spawn(move || {
   |                        ------------- ^------
   |                        |             |
   |  ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
   | |                      |
   | |                      required by a bound introduced by this call
12 | |             let mut num = counter.lock().unwrap();
13 | |
14 | |             *num += 1;
15 | |         });
   | |_________^ `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
   |
   = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`
note: required because it's used within this closure
  --> src/main.rs:11:36
   |
11 |         let handle = thread::spawn(move || {
   |                                    ^^^^^^^
note: required by a bound in `spawn`
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:723:1

For more information about this error, try `rustc --explain E0277`.
error: could not compile `paylasimli-durum` (bin "paylasimli-durum") due to 1 previous error

Bu mesajın en önemli kısmı şudur: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely. Derleyici bunun nedenini de söylüyor: the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`. Bir sonraki bölümde Send trait’ini ayrıntılı konuşacağız. Şimdilik şunu bilin: Send, iş parçacıklarıyla kullanılacak türlerin eşzamanlı bağlamlara uygun olduğunu garanti eden trait’lerden biridir.

Ne yazık ki Rc<T> iş parçacıkları arasında paylaşmak için güvenli değildir. Rc<T> referans sayısını yönettiğinde, her clone çağrısında sayıyı artırır, her klon düşürüldüğünde de azaltır. Fakat bu sayacın güncellenmesi sırasında başka bir iş parçacığının araya girmesini önleyecek bir eşzamanlılık ilkelini kullanmaz. Bu da yanlış sayımlara, dolayısıyla çok sinsi hatalara, bellek sızıntılarına ya da işimiz bitmeden bir değerin düşürülmesine yol açabilir. Burada bize gereken şey, Rc<T>ye çok benzeyen ama referans sayısını iş parçacığı güvenli biçimde güncelleyen bir türdür.

Arc<T> ile Atomik Referans Sayımı

Neyse ki Arc<T>, Rc<T> gibi davranan ama eşzamanlı ortamlarda güvenle kullanılabilen bir türdür. Buradaki a, atomic sözcüğünden gelir; yani bu tür atomik referans sayımlıdır (atomically reference-counted). Atomikler, burada ayrıntısına girmeyeceğimiz ayrı bir eşzamanlılık ilkelidir. Daha fazla bilgi için standart kütüphanedeki std::sync::atomic belgelerine bakabilirsiniz. Şimdilik bilmeniz gereken, atomik türlerin ilkel türler gibi çalıştığı ama iş parçacıkları arasında güvenle paylaşılabildiğidir.

Şu soru akla gelebilir: Madem öyle, neden bütün ilkel türler atomik değil ya da standart kütüphane türleri varsayılan olarak neden Arc<T> kullanmıyor? Cevap şu: İş parçacığı güvenliği bir performans bedeli getirir ve bu bedeli yalnızca gerçekten gerektiğinde ödemek istersiniz. Tek bir iş parçacığında çalışıyorsanız, atomik güvenceleri zorunlu kılmak zorunda kalmayan kod daha hızlı çalışabilir.

Örneğimize geri dönelim: Arc<T> ile Rc<T> aynı API’yi sunar. Bu nedenle programı düzeltmek için yalnızca use satırını, new çağrısını ve clone çağrısını değiştirmemiz yeterlidir. 16-15 numaralı listedeki kod sonunda derlenir ve çalışır.

Filename: src/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);
        let tutamac = thread::spawn(move || {
            let mut sayi = sayac.lock().unwrap();

            *sayi += 1;
        });
        tutamaclar.push(tutamac);
    }

    for tutamac in tutamaclar {
        tutamac.join().unwrap();
    }

    println!("Sonuç: {}", *sayac.lock().unwrap());
}
Listing 16-15: Birden fazla iş parçacığı arasında sahipliği paylaşabilmek için Mutex<T> değerini Arc<T> ile sarmalamak

Bu kod aşağıdaki çıktıyı üretir:

Sonuç: 10

Başardık! 0’dan 10’a kadar saydık. Bu çok etkileyici görünmeyebilir; ama Mutex<T> ve iş parçacığı güvenliği hakkında pek çok şey öğrenmiş olduk. Aslında aynı yapı, yalnızca sayaç artırmak değil çok daha karmaşık hesaplar için de kullanılabilir. Bu stratejiyle bir hesabı bağımsız parçalara bölebilir, parçaları iş parçacıklarına dağıtabilir ve sonunda her parçanın sonucunu Mutex<T> aracılığıyla ortak sonuca yansıtabilirsiniz.

Şunu da unutmayın: Basit sayısal işlemler yapıyorsanız, standart kütüphanedeki std::sync::atomic modülü altında Mutex<T>den daha sade türler bulunur. Bu türler ilkel verilere güvenli, eşzamanlı ve atomik erişim sağlar. Biz burada Mutex<T>yi ilkel bir türle kullanmayı özellikle seçtik; çünkü amacımız öncelikle Mutex<T>nin nasıl çalıştığını göstermekti.

RefCell<T>/Rc<T> ile Mutex<T>/Arc<T> Karşılaştırması

sayac değişkeninin değiştirilemez tanımlandığını ama içindeki değere değiştirilebilir referans alabildiğimizi fark etmiş olabilirsiniz. Bu da Mutex<T>nin, Cell ailesindeki türler gibi içsel değiştirilebilirlik sağladığını gösterir. 15. bölümde Rc<T> içindeki değeri değiştirebilmek için RefCell<T> kullanmıştık; burada da aynı işi Arc<T> içindeki değer için Mutex<T> ile yapıyoruz.

Dikkat edilmesi gereken başka bir nokta daha var: Mutex<T> kullanırken Rust sizi her türlü mantık hatasından koruyamaz. 15. bölümde, Rc<T> kullanırken iki değerin birbirini işaret etmesiyle referans döngüsü kurulabileceğini ve bunun bellek sızıntısına yol açabileceğini görmüştünüz. Benzer biçimde Mutex<T> de kilitlenme (deadlock) riskini taşır. Bu durum, bir işlemin iki kaynak için kilit alması gerektiğinde ve iki iş parçacığının bu kilitlerden birer tanesini alıp birbirini sonsuza kadar beklemesiyle ortaya çıkar.

Kilitlenmeler ilginizi çekiyorsa, bir kilitlenme içeren küçük bir Rust programı yazmayı deneyin. Ardından başka dillerde mutex kullanan sistemlerde uygulanan kilitlenme azaltma stratejilerini araştırın ve benzerini Rust’ta kurmayı deneyin. Standart kütüphanedeki Mutex<T> ve MutexGuard API belgeleri bu konuda yararlı bilgiler içerir.

Bölümü Send ve Sync trait’lerinden ve bunları özel türlerle nasıl kullanabileceğimizden söz ederek tamamlayacağız.