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.
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ı");
}
thread::sleep kullanmakBu 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.
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ı");
}
slow fonksiyonunu çağırmakHer 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.
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ı");
}
trpl::sleep kullanmakHer 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.
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ı");
}
yield_now kullanmakBu 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.
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())
}
}
});
}
timeout kullanımıŞimdi bunu gerçekten yazalım. Önce API’yi düşünelim:
- Kendisi de async fonksiyon olmalı ki onu
awaitedebilelim. - İlk parametresi çalıştırılacak bir future olmalı.
- İkinci parametre beklenecek azami süre olmalı. Bunun için
Durationkullanmak en elverişli yol. - Dönüş türü
Resultolmalı. Future zamanında tamamlanırsaOk, süre dolarsaErrdönmeli.
17-19 numaralı liste bu imzayı gösteriyor.
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!
}
timeout imzasını tanımlamakTü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.
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),
}
}
select ve sleep ile timeout tanımlamaktrpl::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.