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

Kodu Aynı Anda Çalıştırmak İçin İş Parçacıkları Kullanmak

Günümüzde çoğu işletim sisteminde çalışan program kodu bir süreç (process) içinde yürütülür ve işletim sistemi aynı anda birden fazla süreci yönetir. Bir programın içinde de aynı anda ilerleyen bağımsız parçalar olabilir. Bu bağımsız parçaları çalıştıran yapılara iş parçacığı (thread) denir. Örneğin bir web sunucusu, aynı anda birden fazla isteğe yanıt verebilmek için birden çok iş parçacığı kullanabilir.

Programınızdaki hesabı birden fazla iş parçacığına bölüp görevleri aynı anda çalıştırmak performansı artırabilir; ama bunun karşılığında karmaşıklık da artar. İş parçacıkları aynı anda çalışabildiği için, farklı iş parçacıklarında yer alan kodunuzun hangi sırayla işleyeceğine dair doğal bir garanti yoktur. Bu da şu tür sorunlara yol açabilir:

  • İş parçacıklarının veri ya da kaynaklara tutarsız bir sırayla eriştiği yarış durumları
  • İki iş parçacığının birbirini beklediği ve ikisinin de devam edemediği kilitlenmeler
  • Yalnızca bazı durumlarda ortaya çıkan, tekrar üretmesi ve güvenle düzeltmesi zor hatalar

Rust, iş parçacığı kullanmanın olumsuz yanlarını azaltmaya çalışır; ama çok iş parçacıklı programlama yine de dikkatli düşünmeyi ve tek iş parçacıklı programlardan farklı bir kod yapısını gerektirir.

Programlama dilleri iş parçacıklarını çeşitli şekillerde uygular; birçok işletim sistemi de yeni iş parçacığı oluşturmak için çağrılabilen bir API sunar. Rust standart kütüphanesi iş parçacıkları için 1:1 modelini kullanır: Programdaki her dil düzeyi iş parçacığı için bir işletim sistemi iş parçacığı vardır. Farklı ödünleşimler sunan başka iş parçacığı modellerini uygulayan crate’ler de vardır. Bir sonraki bölümde göreceğimiz Rust’ın async sistemi de eşzamanlılığa farklı bir yaklaşım sunar.

spawn ile Yeni Bir İş Parçacığı Oluşturmak

Yeni bir iş parçacığı oluşturmak için thread::spawn fonksiyonunu çağırır ve ona, yeni iş parçacığında çalıştırmak istediğimiz kodu içeren bir kapanış veririz. 16-1 numaralı liste, ana iş parçacığından biraz metin; yeni oluşturulan iş parçacığından da başka metinler yazdırır.

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for sira in 1..10 {
            println!("oluşturulan iş parçacığından merhaba sayı {sira}!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for sira in 1..5 {
        println!("ana iş parçacığından merhaba sayı {sira}!");
        thread::sleep(Duration::from_millis(1));
    }
}
Listing 16-1: Ana iş parçacığı başka bir şey yazdırırken yeni bir iş parçacığı oluşturmak

Rust programında ana iş parçacığı tamamlandığında, oluşturulmuş bütün iş parçacıkları işleri bitmiş olsun ya da olmasın kapatılır. Bu programın çıktısı her çalıştırmada biraz farklı olabilir; ama aşağıdakine benzer görünür:

ana iş parçacığından merhaba sayı 1!
oluşturulan iş parçacığından merhaba sayı 1!
ana iş parçacığından merhaba sayı 2!
oluşturulan iş parçacığından merhaba sayı 2!
ana iş parçacığından merhaba sayı 3!
oluşturulan iş parçacığından merhaba sayı 3!
ana iş parçacığından merhaba sayı 4!
oluşturulan iş parçacığından merhaba sayı 4!
oluşturulan iş parçacığından merhaba sayı 5!

thread::sleep çağrıları bir iş parçacığını kısa süreliğine durdurur ve başka bir iş parçacığının çalışmasına fırsat verir. Çoğu zaman iş parçacıkları sıra ile ilerler; ama bu garanti değildir. Her şey işletim sisteminizin iş parçacıklarını nasıl zamanladığına bağlıdır. Bu çalıştırmada, kodda önce oluşturulan iş parçacığındaki println! görünse de ilk yazdıran ana iş parçacığı oldu. Ayrıca oluşturulan iş parçacığına i değeri 9 olana kadar yazdırmasını söylemiş olsak da, ana iş parçacığı kapanmadan önce ancak 5 sayısına kadar gelebildi.

Bu kodu çalıştırıp yalnızca ana iş parçacığının çıktısını görürseniz ya da hiç örtüşme görmezseniz, aralıkları büyütmeyi deneyin. Böylece işletim sisteminin iki iş parçacığı arasında geçiş yapması için daha fazla fırsat oluşur.

Tüm İş Parçacıklarının Bitmesini Beklemek

16-1 numaralı listedeki kodun sorunu şu: Ana iş parçacığı çoğu zaman daha erken bittiği için oluşturulan iş parçacığı yarıda kesiliyor. Üstelik iş parçacıkları hangi sırayla çalışacak belli olmadığından, oluşturulan iş parçacığının hiç çalışacağı da garanti değil.

Bu sorunu çözmek için thread::spawn dönüş değerini bir değişkende saklayabiliriz. thread::spawn fonksiyonu JoinHandle<T> döndürür. JoinHandle<T>, sahip olunan bir değerdir; bunun üstünde join metodunu çağırdığımızda ilgili iş parçacığının tamamlanmasını bekleriz. 16-2 numaralı liste, 16-1’de oluşturduğumuz iş parçacığı için dönen JoinHandle<T> değerini nasıl kullandığımızı ve main sonlanmadan önce bu iş parçacığının bitmesini garanti etmek için join metodunu nasıl çağırdığımızı gösteriyor.

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn main() {
    let tutamac = thread::spawn(|| {
        for sira in 1..10 {
            println!("oluşturulan iş parçacığından merhaba sayı {sira}!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for sira in 1..5 {
        println!("ana iş parçacığından merhaba sayı {sira}!");
        thread::sleep(Duration::from_millis(1));
    }

    tutamac.join().unwrap();
}
Listing 16-2: İş parçacığının sonuna kadar çalışmasını garanti etmek için thread::spawn dönüşündeki JoinHandle<T> değerini saklamak

tutamac üzerinde join çağırmak, şu anda çalışan iş parçacığını, tutamacın temsil ettiği iş parçacığı bitene kadar bloklar (blocking). Bir iş parçacığının bloklanması, onun iş yapmasının ya da sonlanmasının geçici olarak engellenmesi demektir. join çağrısını ana iş parçacığındaki for döngüsünden sonraya koyduğumuz için, 16-2 numaralı listenin çıktısı şuna benzer:

ana iş parçacığından merhaba sayı 1!
ana iş parçacığından merhaba sayı 2!
oluşturulan iş parçacığından merhaba sayı 1!
ana iş parçacığından merhaba sayı 3!
oluşturulan iş parçacığından merhaba sayı 2!
ana iş parçacığından merhaba sayı 4!
oluşturulan iş parçacığından merhaba sayı 3!
oluşturulan iş parçacığından merhaba sayı 4!
oluşturulan iş parçacığından merhaba sayı 5!
oluşturulan iş parçacığından merhaba sayı 6!
oluşturulan iş parçacığından merhaba sayı 7!
oluşturulan iş parçacığından merhaba sayı 8!
oluşturulan iş parçacığından merhaba sayı 9!

İki iş parçacığı dönüşümlü ilerlemeyi sürdürür; ama tutamac.join() çağrısı nedeniyle ana iş parçacığı sona ermez ve oluşturulan iş parçacığının tamamlanmasını bekler.

Şimdi de tutamac.join() çağrısını main içindeki for döngüsünden önceye taşırsak ne olur, ona bakalım:

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn main() {
    let tutamac = thread::spawn(|| {
        for sira in 1..10 {
            println!("oluşturulan iş parçacığından merhaba sayı {sira}!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    tutamac.join().unwrap();

    for sira in 1..5 {
        println!("ana iş parçacığından merhaba sayı {sira}!");
        thread::sleep(Duration::from_millis(1));
    }
}

Bu durumda ana iş parçacığı önce oluşturulan iş parçacığının bitmesini bekler, sonra kendi for döngüsünü çalıştırır. Yani çıktılar artık iç içe geçmez:

oluşturulan iş parçacığından merhaba sayı 1!
oluşturulan iş parçacığından merhaba sayı 2!
oluşturulan iş parçacığından merhaba sayı 3!
oluşturulan iş parçacığından merhaba sayı 4!
oluşturulan iş parçacığından merhaba sayı 5!
oluşturulan iş parçacığından merhaba sayı 6!
oluşturulan iş parçacığından merhaba sayı 7!
oluşturulan iş parçacığından merhaba sayı 8!
oluşturulan iş parçacığından merhaba sayı 9!
ana iş parçacığından merhaba sayı 1!
ana iş parçacığından merhaba sayı 2!
ana iş parçacığından merhaba sayı 3!
ana iş parçacığından merhaba sayı 4!

join çağrısının nereye yazıldığı gibi küçük ayrıntılar bile iş parçacıklarının gerçekten aynı anda çalışıp çalışmayacağını etkileyebilir.

İş Parçacıklarıyla move Kapanışları Kullanmak

thread::spawn içine verdiğimiz kapanışlarla birlikte çoğu zaman move anahtar sözcüğünü de kullanırız. Çünkü bu durumda kapanış, kullandığı değerlerin sahipliğini çevresinden alır; böylece o değerlerin sahipliği bir iş parçacığından diğerine aktarılmış olur. 13. bölümde “Referansları Yakalamak ya da Sahipliği Taşımak” kısmında move anahtar sözcüğünü kapanışlar bağlamında görmüştük. Şimdi move ile thread::spawn arasındaki ilişkiye odaklanacağız.

16-1 numaralı listedeki kapanışın hiç parametre almadığına dikkat edin: Ana iş parçacığındaki hiçbir veriyi, oluşturulan iş parçacığındaki kodda kullanmıyoruz. Oluşturulan iş parçacığında ana iş parçacığından veri kullanabilmek için, kapanış ihtiyaç duyduğu değerleri yakalamalıdır. 16-3 numaralı liste, ana iş parçacığında bir vektör oluşturup onu oluşturulan iş parçacığında kullanma girişimini gösteriyor. Ama birazdan göreceğiniz gibi bu halde çalışmaz.

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

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

    let tutamac = thread::spawn(|| {
        println!("İşte bir vektör: {vektor:?}");
    });

    tutamac.join().unwrap();
}
Listing 16-3: Ana iş parçacığında oluşturulan bir vektörü başka bir iş parçacığında kullanmaya çalışmak

Kapanış vektor değişkenini kullandığı için onu yakalar ve kendi çevresinin bir parçası haline getirir. thread::spawn bu kapanışı yeni bir iş parçacığında çalıştırdığına göre, sanki bu yeni iş parçacığında vektor değerine erişebilmemiz gerekiyormuş gibi görünür. Fakat bu örneği derlediğimizde şu hatayı alırız:

$ cargo run
   Compiling is-parcaciklari v0.1.0 (file:///projects/is-parcaciklari)
error[E0373]: closure may outlive the current function, but it borrows `vektor`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let tutamac = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `vektor`
7 |         println!("İşte bir vektör: {vektor:?}");
  |                                     ------ `vektor` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let tutamac = thread::spawn(|| {
  |  __________________^
7 | |         println!("İşte bir vektör: {vektor:?}");
8 | |     });
  | |______^
help: to force the closure to take ownership of `vektor` (and any other referenced variables), use the `move` keyword
  |
6 |     let tutamac = thread::spawn(move || {
  |                                ++++

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

Rust, vektor değişkeninin nasıl yakalanacağını çıkarsar (infer). Burada println! yalnızca vektor için bir referansa ihtiyaç duyduğu için kapanış onu ödünç almaya çalışır. Sorun şu ki Rust, oluşturulan iş parçacığının ne kadar yaşayacağını bilemez; bu yüzden vektor için alınan referansın her zaman geçerli kalıp kalmayacağını da bilemez.

16-4 numaralı liste, vektor referansının geçersiz hale gelmesinin çok daha olası olduğu bir senaryo gösterir.

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

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

    let tutamac = thread::spawn(|| {
        println!("İşte bir vektör: {vektor:?}");
    });

    drop(vektor); // eyvah!

    tutamac.join().unwrap();
}
Listing 16-4: Ana iş parçacığı vektor değerini düşürürken, kapanış içinde ona referans yakalamaya çalışan bir iş parçacığı

Rust bu kodu çalıştırmamıza izin verseydi, oluşturulan iş parçacığı hiç çalışmadan hemen arka plana alınabilirdi. O iş parçacığının içinde vektore ait bir referans var; ama ana iş parçacığı 15. bölümde gördüğümüz drop fonksiyonunu kullanarak vektorü anında serbest bırakıyor. Sonra oluşturulan iş parçacığı çalışmaya başladığında vektor artık geçerli olmuyor; dolayısıyla ona ait referans da geçersiz hale geliyor. Kötü haber!

16-3 numaralı listedeki derleyici hatasını düzeltmek için hata mesajındaki öneriyi uygulayabiliriz:

help: to force the closure to take ownership of `vektor` (and any other referenced variables), use the `move` keyword
  |
6 |     let tutamac = thread::spawn(move || {
  |                                ++++

Kapanışın önüne move eklediğimizde, Rust’ın ödünç alma çıkarımı yapmasına izin vermek yerine kullanılan değerlerin sahipliğini kapanışın almasını zorlamış oluruz. 16-3 numaralı listedeki örneğin 16-5’te gösterilen bu değiştirilmiş hali, istediğimiz gibi derlenir ve çalışır.

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

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

    let tutamac = thread::spawn(move || {
        println!("İşte bir vektör: {vektor:?}");
    });

    tutamac.join().unwrap();
}
Listing 16-5: Bir kapanışın kullandığı değerlerin sahipliğini almasını zorlamak için move kullanmak

16-4 numaralı listedeki kodu da aynı yolla düzeltmeyi düşünebiliriz. Orada ana iş parçacığı drop çağrısıyla vektorü düşürüyordu. Fakat 16-5’teki gibi move kullandığımız anda, vektorün sahipliği kapanışa aktarılır. Bu yüzden ana iş parçacığında artık drop(vektor) çağırmak mümkün olmaz. Bunu denediğimizde, bu kez aşağıdaki gibi farklı bir derleyici hatası alırız:

$ cargo run
   Compiling is-parcaciklari v0.1.0 (file:///projects/is-parcaciklari)
error[E0382]: use of moved value: `vektor`
  --> src/main.rs:10:10
   |
 4 |     let vektor = vec![1, 2, 3];
   |         ------ move occurs because `vektor` has type `Vec<i32>`, which does not implement the `Copy` trait
 5 |
 6 |     let tutamac = thread::spawn(move || {
   |                                 ------- value moved into closure here
 7 |         println!("İşte bir vektör: {vektor:?}");
   |                                     ------ variable moved due to use in closure
...
10 |     drop(vektor); // eyvah!
   |          ^^^^^^ value used here after move
   |
help: consider cloning the value before moving it into the closure
   |
 6 ~     let value = vektor.clone();
 7 ~     let tutamac = thread::spawn(move || {
 8 ~         println!("İşte bir vektör: {value:?}");
   |

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

Rust’ın sahiplik kuralları bizi yine korumuş oldu.