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

Denetimi Çalışma Zamanına Geri Vermek

“İlk Async Programımız” kısmından hatırlayın: Her await noktasında Rust, beklenen future hazır değilse çalışma zamanına görevi durdurup başka bir işe geçme fırsatı verir. Tersi de doğrudur: Rust, async blokları yalnızca await noktalarında durdurur ve denetimi çalışma zamanına geri verir. await noktaları arasındaki her şey senkrondur.

Bu şu anlama gelir: Bir async blok içinde await olmadan uzun süre iş yaparsanız, o future başka future’ların ilerlemesini engeller. Bazen buna bir future’ın ötekileri aç bırakması denir. Bazı durumlarda bu büyük sorun olmayabilir. Ama pahalı bir hazırlık işi yapıyorsanız, uzun süren bir hesap koşturuyorsanız ya da bir future belirli bir işi sonsuza kadar sürdürecekse, denetimi çalışma zamanına ne zaman geri vereceğinizi dikkatle düşünmeniz gerekir.

Bunu göstermek için uzun süren bir işlemi taklit edelim. 17-14 numaralı liste, slow fonksiyonunu tanıtıyor.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        // We will call `slow` here later
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' {ms}ms çalıştı");
}
Listing 17-14: Yavaş işlemleri taklit etmek için thread::sleep kullanmak

Bu kodda trpl::sleep yerine std::thread::sleep kullanıyoruz; böylece slow çağrısı mevcut iş parçacığını gerçekten bloke ediyor. Böylece slow gerçek dünyadaki hem uzun süren hem bloklayıcı işlemleri temsil edebiliyor.

17-15 numaralı listede bu slow fonksiyonunu, iki future içinde CPU-bağımlı iş yürütmeyi taklit etmek için kullanıyoruz.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let a = async {
            println!("'a' başladı.");
            slow("a", 30);
            slow("a", 10);
            slow("a", 20);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'a' bitti.");
        };

        let b = async {
            println!("'b' başladı.");
            slow("b", 75);
            slow("b", 10);
            slow("b", 15);
            slow("b", 350);
            trpl::sleep(Duration::from_millis(50)).await;
            println!("'b' bitti.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' {ms}ms çalıştı");
}
Listing 17-15: Yavaş işlemleri taklit etmek için slow fonksiyonunu çağırmak

Her future, bir sürü yavaş işi yaptıktan sonra çalışma zamanına denetim veriyor. Bu kodu çalıştırdığınızda aşağıdaki çıktıyı görürsünüz:

'a' başladı.
'a' 30ms çalıştı
'a' 10ms çalıştı
'a' 20ms çalıştı
'b' başladı.
'b' 75ms çalıştı
'b' 10ms çalıştı
'b' 15ms çalıştı
'b' 350ms çalıştı
'a' bitti.

17-5 numaralı listede iki URL’yi yarıştırmak için trpl::select kullanmıştık. Burada da select, a biter bitmez tamamlanıyor. Ama iki future içindeki slow çağrıları birbirine karışmıyor. a future’ı, trpl::sleep noktasına gelene kadar bütün işini yapıyor; sonra b kendi trpl::sleep noktasına kadar çalışıyor; ardından a tamamen bitiyor. Her iki future’ın da ağır işleri arasında ilerleme kaydedebilmesi için await noktalarına ihtiyacımız var. Yani await edebileceğimiz bir şeye ihtiyacımız var!

17-15’te bunu kısmen zaten görüyoruz: a future’ının sonundaki trpl::sleep çağrısını kaldırırsanız, b hiç çalışmadan a tamamlanır. O halde ilerlemeyi parçalara bölmek için şimdilik trpl::sleep kullanmayı deneyelim; 17-16 numaralı liste bunu gösteriyor.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let bir_ms = Duration::from_millis(1);

        let a = async {
            println!("'a' başladı.");
            slow("a", 30);
            trpl::sleep(bir_ms).await;
            slow("a", 10);
            trpl::sleep(bir_ms).await;
            slow("a", 20);
            trpl::sleep(bir_ms).await;
            println!("'a' bitti.");
        };

        let b = async {
            println!("'b' başladı.");
            slow("b", 75);
            trpl::sleep(bir_ms).await;
            slow("b", 10);
            trpl::sleep(bir_ms).await;
            slow("b", 15);
            trpl::sleep(bir_ms).await;
            slow("b", 350);
            trpl::sleep(bir_ms).await;
            println!("'b' bitti.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' {ms}ms çalıştı");
}
Listing 17-16: İşlemlerin sırayla ilerlemesini sağlamak için trpl::sleep kullanmak

Her slow çağrısının arasına trpl::sleep ve bir await noktası ekledik. Böylece iki future’ın işi iç içe geçiyor:

'a' başladı.
'a' 30ms çalıştı
'b' başladı.
'b' 75ms çalıştı
'a' 10ms çalıştı
'b' 10ms çalıştı
'a' 20ms çalıştı
'b' 15ms çalıştı
'a' bitti.

a hâlâ ilk trpl::sleep çağrısına kadar biraz önden gidiyor; çünkü ilk slow çalışmadan önce hiç await etmiyor. Ama ondan sonra iki future, her await noktasında sırayla denetim değiştiriyor. İşi istediğimiz anlamlı parçalara bölmek tamamen bize kalmış.

Aslında burada uyumak istemiyoruz; olabildiğince hızlı ilerlemek istiyoruz. Tek ihtiyacımız çalışma zamanına denetimi geri vermek. Bunun için doğrudan trpl::yield_now kullanabiliriz. 17-17 numaralı listede bütün trpl::sleep çağrılarını bununla değiştiriyoruz.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::{thread, time::Duration};

fn main() {
    trpl::block_on(async {
        let a = async {
            println!("'a' başladı.");
            slow("a", 30);
            trpl::yield_now().await;
            slow("a", 10);
            trpl::yield_now().await;
            slow("a", 20);
            trpl::yield_now().await;
            println!("'a' bitti.");
        };

        let b = async {
            println!("'b' başladı.");
            slow("b", 75);
            trpl::yield_now().await;
            slow("b", 10);
            trpl::yield_now().await;
            slow("b", 15);
            trpl::yield_now().await;
            slow("b", 350);
            trpl::yield_now().await;
            println!("'b' bitti.");
        };

        trpl::select(a, b).await;
    });
}

fn slow(name: &str, ms: u64) {
    thread::sleep(Duration::from_millis(ms));
    println!("'{name}' {ms}ms çalıştı");
}
Listing 17-17: İlerlemeyi sırayla sürdürmek için yield_now kullanmak

Bu sürüm hem niyetimizi daha açık anlatır hem de çoğu zaman sleep kullanmaktan daha hızlıdır. Çünkü sleep’in dayandığı zamanlayıcıların çözünürlüğü çoğu zaman sınırlıdır. Kullandığımız sleep sürümü örneğin bir nanosaniye verseniz bile en az bir milisaniye uyur. Modern bilgisayarlar için bir milisaniye çok uzundur.

Bu da async’in, programın başka neler yaptığına bağlı olarak, CPU-bağımlı işlerde bile yararlı olabileceğini gösterir. Çünkü kodun farklı parçaları arasındaki ilişkiyi kurmak için kullanışlı bir yapı sağlar. Bunun bedeli, async durum makinesinin ek maliyetidir. Bu yaklaşım, işbirlikli çoklu görev biçimidir: her future, await noktaları sayesinde denetimi ne zaman teslim edeceğine kendi karar verir. Dolayısıyla çok uzun süre bloklamamak da onun sorumluluğudur.

Gerçek dünyada elbette her fonksiyon çağrısının arasına bir await koymazsınız. Bu biçimde denetim devretmek ucuzdur ama bedelsiz değildir. Bazı durumlarda CPU-bağımlı işi küçük parçalara bölmek genel performansı düşürebilir. Yine de beklediğiniz eşzamanlılığın neden seri çalıştığını anlamak için bu dinamiği akılda tutmak önemlidir.

Kendi Async Soyutlamalarımızı Kurmak

Future’ları birleştirerek yeni desenler de oluşturabiliriz. Örneğin elimizdeki async yapı taşlarıyla bir timeout fonksiyonu kurabiliriz. Bu bittiğinde, o da başka async soyutlamalar oluşturmakta kullanabileceğimiz yeni bir yapı taşı haline gelir.

17-18 numaralı liste, bu hayali timeout fonksiyonunun yavaş bir future ile nasıl davranmasını beklediğimizi gösteriyor.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let yavas = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Sonunda bitti"
        };

        match timeout(yavas, Duration::from_secs(2)).await {
            Ok(message) => println!("'{message}' ile başarılı oldu"),
            Err(duration) => {
                println!("{} saniye sonra başarısız oldu", duration.as_secs())
            }
        }
    });
}
Listing 17-18: Zaman sınırıyla yavaş bir işlemi çalıştırmak için hayalî timeout kullanımı

Şimdi bunu gerçekten yazalım. Önce API’yi düşünelim:

  • Kendisi de async fonksiyon olmalı ki onu await edebilelim.
  • İlk parametresi çalıştırılacak bir future olmalı.
  • İkinci parametre beklenecek azami süre olmalı. Bunun için Duration kullanmak en elverişli yol.
  • Dönüş türü Result olmalı. Future zamanında tamamlanırsa Ok, süre dolarsa Err dönmeli.

17-19 numaralı liste bu imzayı gösteriyor.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

fn main() {
    trpl::block_on(async {
        let yavas = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Sonunda bitti"
        };

        match timeout(yavas, Duration::from_secs(2)).await {
            Ok(message) => println!("'{message}' ile başarılı oldu"),
            Err(duration) => {
                println!("{} saniye sonra başarısız oldu", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    denecek_gelecek: F,
    azami_sure: Duration,
) -> Result<F::Output, Duration> {
    // Uygulamamiz buraya gelecek!
}
Listing 17-19: timeout imzasını tanımlamak

Türler tamam. Şimdi davranışı düşünelim: Parametre olarak gelen future ile süreyi yarıştırmak istiyoruz. Süreden bir zamanlayıcı future üretmek için trpl::sleep, ikisini yarıştırmak için de trpl::select kullanabiliriz.

17-20 numaralı listede timeout, trpl::select sonucunu eşleştirerek gerçekleniyor.

Filename: src/main.rs
extern crate trpl; // required for mdbook test

use std::time::Duration;

use trpl::Either;

// --snip--

fn main() {
    trpl::block_on(async {
        let yavas = async {
            trpl::sleep(Duration::from_secs(5)).await;
            "Sonunda bitti"
        };

        match timeout(yavas, Duration::from_secs(2)).await {
            Ok(message) => println!("'{message}' ile başarılı oldu"),
            Err(duration) => {
                println!("{} saniye sonra başarısız oldu", duration.as_secs())
            }
        }
    });
}

async fn timeout<F: Future>(
    denecek_gelecek: F,
    azami_sure: Duration,
) -> Result<F::Output, Duration> {
    match trpl::select(denecek_gelecek, trpl::sleep(azami_sure)).await {
        Either::Left(output) => Ok(output),
        Either::Right(_) => Err(azami_sure),
    }
}
Listing 17-20: select ve sleep ile timeout tanımlamak

trpl::select uygulaması adil değildir; argümanları geçtiğiniz sırayla yoklar. Bu yüzden denecek_gelecek değerini ilk argüman olarak veriyoruz; böylece azami_sure çok kısa olsa bile ana future’ın önce bir şansı olur. Eğer denecek_gelecek önce biterse select, Left ile onun çıktısını döndürür. Zamanlayıcı önce biterse Right ile () döner.

Eğer ana future başarıyla tamamlandıysa Ok(output) döndürürüz. Süre önce dolduysa Right(()) içindeki () değerini yok sayıp Err(azami_sure) döndürürüz.

Böylece başka iki async yardımcıdan yararlanarak çalışan bir timeout elde ettik. Kodu çalıştırdığımızda zaman aşımı nedeniyle başarısız çıktıyı görürüz:

2 saniye sonra başarısız oldu

Future’lar başka future’larla birleştirilebildiği için, küçük async yapı taşlarından çok güçlü araçlar kurabilirsiniz. Örneğin aynı yaklaşımı zaman aşımı ile yeniden denemeyi birleştirmek için kullanabilir, sonra bunu ağ çağrıları gibi işlemlere uygulayabilirsiniz.

Pratikte çoğu zaman doğrudan async ile await kullanır, ikinci adımda da select gibi fonksiyonlar veya join! gibi makrolarla en dıştaki future’ların nasıl yürütüleceğini kontrol edersiniz.

Şimdiye kadar aynı anda birden fazla future ile çalışmanın farklı yollarını gördük. Sırada, zaman içinde art arda gelen çok sayıda future-benzeri öğeyi akışlar ile ele almak var.