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 Thread’leri Kullanma

Günümüzdeki çoğu işletim sisteminde çalıştırılan bir programın 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 kendi içinde de aynı anda çalışan bağımsız bölümler bulunabilir. Bu bağımsız bölümleri çalıştıran yapılara thread (iş parçacığı) denir. Örneğin bir web sunucusu, aynı anda birden fazla isteğe yanıt verebilmek için birden çok thread kullanabilir.

Programınızdaki hesaplamayı birden fazla görevi aynı anda yürütecek şekilde birden çok thread’e bölmek performansı artırabilir, ancak beraberinde karmaşıklık da getirir. Thread’ler aynı anda çalışabildiği için, farklı thread’lerdeki kod parçalarının hangi sırayla çalışacağına dair doğal bir garanti yoktur. Bu durum aşağıdaki gibi sorunlara yol açabilir:

  • Thread’lerin verilere veya kaynaklara tutarsız bir sırayla eriştiği yarış koşulları (race conditions)
  • İki thread’in birbirini beklediği ve her ikisinin de devam etmesini engelleyen kilitlenmeler (deadlocks)
  • Yalnızca belirli durumlarda ortaya çıkan, yeniden üretilmesi ve güvenilir biçimde düzeltilmesi zor hatalar

Rust, thread kullanmanın olumsuz etkilerini azaltmaya çalışır; ancak çok thread’li bir bağlamda programlama yapmak yine de dikkatli düşünmeyi ve tek bir thread üzerinde çalışan programlardan farklı bir kod yapısını gerektirir.

Programlama dilleri thread’leri birkaç farklı şekilde uygular ve pek çok işletim sistemi, programlama dilinin yeni thread’ler oluşturmak için çağırabileceği bir API sunar. Rust standart kütüphanesi 1:1 thread uygulama modelini kullanır; yani program, dil düzeyindeki her bir thread için bir işletim sistemi thread’i kullanır. 1:1 modeline kıyasla farklı ödünleşimler sunan başka thread modellerini uygulayan crate’ler de mevcuttur. (Bir sonraki bölümde göreceğimiz üzere, Rust’ın async sistemi de eşzamanlılığa farklı bir yaklaşım sunar.)

spawn ile Yeni Bir Thread Oluşturma

Yeni bir thread oluşturmak için thread::spawn fonksiyonunu çağırır ve ona yeni thread içinde çalıştırmak istediğimiz kodu içeren bir kapanış (closure) (kapanışları Bölüm 13’te ele almıştık) aktarırız. Liste 16-1’deki örnek, ana thread’den bazı metinler ve yeni bir thread’den başka metinler yazdırır.

Dosya adı: 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));
    }
}
Liste 16-1: Ana thread başka bir şey yazdırırken farklı bir metin yazdırmak için yeni bir thread oluşturma

Bir Rust programının ana thread’i tamamlandığında, oluşturulan tüm thread’lerin çalışmaları bitmiş olsun ya da olmasın kapatıldığına dikkat edin. Bu programın çıktısı her çalıştırmada biraz farklı olabilir, ancak aşağıdakine benzer görünecektir:

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 thread’i yürütmesini kısa bir süreliğine durdurmaya zorlayarak farklı bir thread’in çalışmasına olanak tanır. Thread’ler muhtemelen sırayla çalışacaktır ancak bunun bir garantisi yoktur: bu durum tamamen işletim sisteminizin thread’leri nasıl zamanladığına bağlıdır. Bu çalıştırmada, oluşturulan thread’deki yazdırma ifadesi kodda daha önce yer almasına rağmen ilk çıktıyı ana thread verdi. Ayrıca oluşturulan thread’e sira değeri 9 olana kadar yazdırmasını söylemiş olmamıza rağmen, ana thread kapanmadan önce yalnızca 5 değerine kadar gelebildi.

Bu kodu çalıştırdığınızda yalnızca ana thread’den gelen çıktıları görürseniz veya çıktılar arasında hiçbir örtüşme göremezseniz, işletim sisteminin thread’ler arasında geçiş yapmasına daha fazla fırsat tanımak için aralıklardaki sayıları artırmayı deneyin.

Tüm Thread’lerin Bitmesini Bekleme

Liste 16-1’deki kod, ana thread sona erdiği için çoğu zaman oluşturulan thread’i erken durdurmakla kalmaz; thread’lerin hangi sırayla çalışacağına dair bir garanti olmadığından, oluşturulan thread’in hiç çalışacağını bile garanti edemeyiz!

Oluşturulan thread’in hiç çalışmaması ya da erken sonlanması sorununu, thread::spawn fonksiyonunun dönüş değerini bir değişkende saklayarak çözebiliriz. thread::spawn fonksiyonunun dönüş türü JoinHandle<T>’dir. JoinHandle<T>, üzerinde join metodunu çağırdığımızda ilgili thread’in bitmesini bekleyen sahipli bir değerdir. Liste 16-2, Liste 16-1’de oluşturduğumuz thread’in JoinHandle<T> değerinin nasıl kullanılacağını ve main sonlanmadan önce oluşturulan thread’in tamamlandığından emin olmak için join metodunun nasıl çağrılacağını göstermektedir.

Dosya adı: 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();
}
Liste 16-2: Thread’in sonuna kadar çalışmasını güvence altına almak için thread::spawn çağrısından dönen JoinHandle<T> değerini saklama

tutamac üzerinde join çağırmak, tutamacın temsil ettiği thread sonlanana kadar o sırada çalışmakta olan thread’i bloklar. Bir thread’i bloklamak (blocking), o thread’in iş yapmasının veya sonlanmasının engellenmesi anlamına gelir. join çağrısını ana thread’in for döngüsünden sonraya koyduğumuz için, Liste 16-2’yi çalıştırmak şuna benzer bir çıktı üretecektir:

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 thread dönüşümlü çalışmaya devam eder, ancak tutamac.join() çağrısı sayesinde ana thread bekler ve oluşturulan thread işini bitirene kadar sonlanmaz.

Peki bunun yerine tutamac.join() çağrısını main içindeki for döngüsünden önceye taşırsak ne olacağına bakalım:

Dosya adı: 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));
    }
}

Ana thread, oluşturulan thread’in bitmesini bekleyecek ve ardından kendi for döngüsünü çalıştıracaktır; dolayısıyla çıktı burada gösterildiği gibi artık iç içe geçmiş olmayacaktır:

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 metodunun nerede çağrıldığı gibi küçük ayrıntılar, thread’lerinizin aynı anda çalışıp çalışmayacağını etkileyebilir.

Not: JoinHandle<T> türündeki <T> parametresi, thread::spawn içine verdiğiniz kapanışın dönüş değerinin türünü temsil eder (yukarıdaki örneklerde kapanış bir değer döndürmediği için bu tür JoinHandle<()> olur). join metodu ise std::thread::Result<T> döndürür: oluşturulan thread sorunsuz tamamlandığında kapanışın ürettiği değer Ok(deger) içinde döner; ancak oluşturulan thread çalışırken paniklerse (panic!), bu panik ana thread’i kendiliğinden çökertmez ve join çağrısı Err döndürür. Örneklerde tutamac.join().unwrap() yazmamızın nedeni budur: alt thread’de bir panik meydana gelmişse unwrap() çağrısı hatayı yakalayıp ana thread’i de panikletir; böylece arka plandaki çökmelerin sessizce gözden kaçması önlenir.

Thread’lerle move Kapanışlarını Kullanma

thread::spawn fonksiyonuna aktarılan kapanışlarla birlikte sıklıkla move anahtar kelimesini kullanırız; çünkü bu sayede kapanış, çevresinden kullandığı değerlerin sahipliğini alır ve böylece bu değerlerin sahipliğini bir thread’den diğerine devretmiş olur. Bölüm 13’teki “Referansları Yakalamak veya Sahipliği Taşımak” başlığında move anahtar kelimesini kapanışlar bağlamında ele almıştık. Şimdi ise move ile thread::spawn arasındaki etkileşime daha yakından odaklanacağız.

Liste 16-1’de thread::spawn fonksiyonuna aktardığımız kapanışın hiçbir argüman almadığına dikkat edin: oluşturulan thread’in kodunda ana thread’den hiçbir veri kullanmıyoruz. Ana thread’deki verileri oluşturulan thread içinde kullanabilmek için, oluşturulan thread’in kapanışının ihtiyaç duyduğu değerleri yakalaması gerekir. Liste 16-3, ana thread’de bir vektör oluşturup onu oluşturulan thread’de kullanma girişimini göstermektedir. Ancak birazdan göreceğiniz üzere bu kod henüz çalışmayacaktır.

Dosya adı: 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();
}
Liste 16-3: Ana thread tarafından oluşturulan bir vektörü başka bir thread’de kullanmaya çalışma

Kapanış vektor değişkenini kullandığı için vektor’ü yakalar ve kapanışın çevresinin bir parçası hâline getirir. thread::spawn bu kapanışı yeni bir thread içinde çalıştırdığından, yeni thread’in içinden vektor değerine erişebilmemiz gerekir. Ancak bu örneği derlediğimizde aşağıdaki 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:33
  |
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:19
  |
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şkenini nasıl yakalayacağını çıkarsar (infer) ve println! yalnızca vektor’e ait bir referansa ihtiyaç duyduğu için kapanış vektor’ü ödünç almaya çalışır. Ancak ortada bir sorun vardır: Rust, oluşturulan thread’in ne kadar süre çalışacağını bilemez; dolayısıyla vektor referansının her zaman geçerli kalıp kalmayacağını da bilemez.

Liste 16-4, vektor referansının geçersiz kalma olasılığının çok daha yüksek olduğu bir senaryoyu göstermektedir.

Dosya adı: 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();
}
Liste 16-4: vektor değerini düşüren ana thread’den vektor referansı yakalamaya çalışan bir kapanışa sahip thread

Rust bu kodu çalıştırmamıza izin verseydi, oluşturulan thread hiç çalışmadan anında arka plana alınabilirdi. Oluşturulan thread kendi içinde vektor için bir referans tutar, ancak ana thread Bölüm 15’te ele aldığımız drop fonksiyonunu kullanarak vektor’ü hemen bellekten düşürür. Ardından oluşturulan thread çalışmaya başladığında vektor artık geçerli değildir; dolayısıyla ona işaret eden referans da geçersizdir. Eyvah!

Liste 16-3’teki derleyici hatasını düzeltmek için hata mesajındaki tavsiyeyi 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ıştan önce move anahtar kelimesini ekleyerek, Rust’ın değerleri ödünç alması gerektiğini çıkarsamasına izin vermek yerine kapanışı kullandığı değerlerin sahipliğini almaya zorlarız. Liste 16-3’teki kodda Liste 16-5’te gösterilen değişikliği yaptığımızda, kod amaçladığımız gibi derlenecek ve çalışacaktır.

Dosya adı: 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();
}
Liste 16-5: Bir kapanışı kullandığı değerlerin sahipliğini almaya zorlamak için move anahtar kelimesini kullanma

Ana thread’in drop çağırdığı Liste 16-4’teki kodu düzeltmek için de aynı şeyi yapmayı, yani bir move kapanışı kullanmayı denemek isteyebiliriz. Ancak bu düzeltme işe yaramayacaktır; çünkü Liste 16-4’ün yapmaya çalıştığı şeye farklı bir nedenden ötürü izin verilmez. Kapanışa move eklersek vektor değerini kapanışın çevresine taşımış oluruz ve artık ana thread içinde onun üzerinde drop çağıramayız. Bunun yerine şu derleyici hatasını 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 bir kez daha kurtardı! Liste 16-3’teki koddan hata almamızın nedeni, Rust’ın temkinli davranarak thread için vektor değerini yalnızca ödünç almasıydı; bu da ana thread’in teorik olarak oluşturulan thread’in referansını geçersiz kılabileceği anlamına geliyordu. Rust’a vektor’ün sahipliğini oluşturulan thread’e taşımasını söyleyerek, ana thread’in vektor’ü bir daha kullanmayacağını Rust’a garanti etmiş oluruz. Liste 16-4’ü aynı şekilde değiştirdiğimizde ise vektor’ü ana thread’de kullanmaya çalıştığımız anda sahiplik kurallarını ihlal etmiş oluruz. move anahtar kelimesi, Rust’ın ödünç alma yönündeki temkinli varsayılan davranışını geçersiz kılar; sahiplik kurallarını çiğnememize izin vermez.

Not: Liste 16-3’ün derleyici çıktısındaki note: function requires argument type to outlive 'static (fonksiyon, argüman türünün 'static ömrü kadar yaşamasını gerektiriyor) açıklamasına dikkat edin. std::thread::spawn fonksiyonu, başlatılan thread’in kendisini çağıran fonksiyondan daha uzun yaşayabileceği varsayımıyla F: Send + 'static sınırı koyar; bu yüzden kısa ömürlü yerel değişkenlere referans alınmasına izin vermez.

Oysa oluşturacağınız thread’lerin belirli bir kod kapsamı bitmeden kesinlikle tamamlanacağını biliyorsanız, verinin sahipliğini taşımak (move), klonlamak veya Arc<T> kullanmak yerine Rust 1.63 ile standart kütüphaneye eklenen std::thread::scope (kapsamlı thread’ler) API’sinden yararlanabilirsiniz. thread::scope, kendi kapsam bloğu sona ermeden önce içeride başlatılan tüm thread’lerin tamamlanmasını otomatik olarak bekler (join eder). Böylece ödünç alma denetleyicisi, yerel değişken referanslarının thread’ler çalıştığı sürece geçerli kalacağını derleme zamanında kanıtlayabilir:

use std::thread;

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

    thread::scope(|kapsam| {
        kapsam.spawn(|| {
            println!("1. thread vektörü ödünç aldı: {vektor:?}");
        });

        kapsam.spawn(|| {
            println!("2. thread vektörü ödünç aldı: {vektor:?}");
        });
    }); // Tüm kapsamlı thread'ler bu noktada otomatik olarak beklenir (join edilir).

    // Sahiplik taşınmadığı için `vektor` ana thread'de hâlâ kullanılabilir:
    println!("Ana thread vektörü kullanmaya devam ediyor: {vektor:?}");
}

Thread’lerin ne olduğunu ve thread API’sinin sunduğu metotları ele aldığımıza göre, şimdi thread’leri kullanabileceğimiz bazı durumlara bakalım.