Future’lar ve Async Sözdizimi
Rust’ta eşzamansız programlamanın temel taşları future yapısı ile async ve
await anahtar sözcükleridir.
Bir future, şu anda hazır olmayabilen ama ileride bir noktada hazır olacak
değerdir. Aynı kavram başka dillerde bazen task ya da promise gibi
isimlerle de karşınıza çıkar. Rust, farklı eşzamansız işlemlerin farklı veri
yapılarıyla gerçekleştirilebilmesini ama yine de ortak bir arayüzle
çalışabilmesini sağlamak için Future trait’ini sunar. Rust’ta future’lar,
Future trait’ini uygulayan türlerdir. Her future, ne kadar ilerlediğine ve
“hazır” olmanın ne anlama geldiğine dair kendi durum bilgisini taşır.
async anahtar sözcüğünü bloklara ve fonksiyonlara uygulayarak bunların
duraklatılıp yeniden sürdürülebileceğini belirtirsiniz. Bir async blok ya da
async fonksiyon içinde ise await kullanarak bir future’ı bekleyebilirsiniz.
Bir async blok ya da fonksiyon içinde future beklediğiniz her nokta, o bloğun
veya fonksiyonun duraklayıp yeniden devam edebileceği potansiyel yerdir.
Bir future’ın değerinin hazır olup olmadığını yoklama sürecine polling denir.
C# ve JavaScript gibi başka diller de async ile await anahtar sözcüklerini
kullanır. Bu dillere aşinaysanız Rust’ın sözdizimi ve davranışında dikkate değer
farklar olduğunu görebilirsiniz. Birazdan bunun nedenini de anlayacağız.
Pratikte async Rust yazarken çoğu zaman async ile await kullanırız. Rust
bunları, tıpkı for döngülerini Iterator trait’i üzerinden eşdeğer koda
çevirdiği gibi, Future trait’ini kullanan eşdeğer koda derler. Ama Rust bize
Future trait’ini sunduğu için, gerektiğinde bu trait’i kendi veri türleriniz
için de uygulayabilirsiniz.
Bu anlatım biraz soyut kalmış olabilir. O yüzden ilk async programımızı yazalım: küçük bir web kazıyıcı. Komut satırından iki URL alacak, ikisini de eşzamanlı olarak isteyecek ve hangisi önce biterse onun sonucunu döndürecek.
İlk Async Programımız
Bu bölümde odağı ekosistemin ayrıntılarına değil, async öğrenmeye vermek için
trpl crate’ini kullandık. trpl, başta futures ve tokio olmak üzere ihtiyaç duyacağınız türleri,
trait’leri ve fonksiyonları yeniden dışa aktarır. futures crate’i Rust’ın
async denemeleri için resmî yuvalardan biridir ve Future trait’i de ilk kez
orada tasarlandı. Tokio ise bugün Rust dünyasında özellikle web uygulamaları
için en yaygın async çalışma zamanıdır.
Bazen trpl, bölümde önemli olmayan ayrıntılarla dikkatimizin dağılmaması için
orijinal API’leri yeniden adlandırır ya da sarmalar. Nasıl çalıştığını görmek
isterseniz kaynak koduna bakabilirsiniz.
hello-async adında yeni bir ikili proje oluşturup trpl bağımlılığını
ekleyin:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
Şimdi trpl’nin sunduğu parçalarla ilk async programımızı yazabiliriz.
İki web sayfasını alacak, her birinin <title> etiketini çıkaracak ve hangisi
önce biterse onun başlığını yazdıran küçük bir komut satırı aracı kuracağız.
sayfa_basligi Fonksiyonunu Tanımlamak
İlk olarak, bir sayfanın URL’sini parametre olarak alan, sayfaya istek yapan ve
<title> etiketindeki metni döndüren bir fonksiyon yazalım.
extern crate trpl; // required for mdbook test
fn main() {
// TODO: we'll add this next!
}
use trpl::Html;
async fn sayfa_basligi(url: &str) -> Option<String> {
let yanit = trpl::get(url).await;
let yanit_metni = yanit.text().await;
Html::parse(&yanit_metni)
.select_first("title")
.map(|title| title.inner_html())
}
Önce sayfa_basligi adında bir fonksiyon tanımlıyor ve onu async ile
işaretliyoruz. Sonra kendisine verilen URL’yi almak için trpl::get
fonksiyonunu çağırıyor, yanıtı beklemek için await kullanıyoruz. Ardından
yanıtın gövdesini metne çevirmek için text metodunu çağırıyor ve onu da
bekliyoruz. Bu iki adımın ikisi de eşzamansızdır.
Bu future’ların ikisini de açıkça beklemek zorundayız; çünkü Rust’ta future’lar
tembeldir. Yani siz await etmeden hiçbir şey yapmazlar. Bu size 13.
bölümdeki yineleyicileri hatırlatabilir: yineleyiciler de next çağrısı
olmadan ilerlemez.
Not: Bu davranış, 16. bölümde
thread::spawnile gördüğümüzden farklıdır. Orada yeni iş parçacığına verdiğimiz kapanış hemen çalışmaya başlamıştı. Rust’ın performans güvencelerini koruyabilmesi için future’ların tembel olması önemlidir.
yanit_metni elimizde olduğunda, onu Html::parse ile Html türüne çevirip
ham dizgi yerine daha zengin bir veri yapısı üzerinde çalışıyoruz. Özellikle
select_first("title") ile ilk <title> öğesini buluyoruz. Böyle bir öğe
olmayabileceği için sonuç Option<ElementRef> olur. Son olarak map
kullanarak varsa başlık içeriğini String olarak çıkarıyoruz. Sonuçta elimizde
Option<String> olur.
Rust’ta await anahtar sözcüğünün, beklenen ifadenin önüne değil sonuna
geldiğine dikkat edin. Yani sonek biçimindedir. Bu, metot zincirlerini daha
rahat yazabilmemizi sağlar. Nitekim 17-2 numaralı listedeki gibi trpl::get
ve text çağrılarını tek zincirde de kullanabiliriz.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
// TODO: we'll add this next!
}
async fn sayfa_basligi(url: &str) -> Option<String> {
let yanit_metni = trpl::get(url).await.text().await;
Html::parse(&yanit_metni)
.select_first("title")
.map(|title| title.inner_html())
}
await anahtar sözcüğüyle zincirleme çağrı yapmakBöylece ilk async fonksiyonumuzu yazmış olduk. Şimdi main içinde bunu
çağırmadan önce, derleyicinin bu kodu nasıl gördüğüne kısaca bakalım.
Rust, async ile işaretlenmiş bir blok gördüğünde, onu Future trait’ini
uygulayan benzersiz ve adsız bir veri türüne dönüştürür. async ile işaretli
bir fonksiyon gördüğünde ise, gövdesi bir async blok olan normal bir
fonksiyona çevirir. Async fonksiyonun dönüş türü de derleyicinin o async blok
için oluşturduğu adsız veri türüdür.
Bu yüzden async fn yazmak, aslında “dönüş türü future olan bir fonksiyon”
yazmakla eşdeğerdir. Derleyici açısından 17-1’deki async fn sayfa_basligi
aşağı yukarı şuna denk gelir:
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;
fn sayfa_basligi(url: &str) -> impl Future<Output = Option<String>> {
async move {
let metin = trpl::get(url).await.text().await;
Html::parse(&metin)
.select_first("title")
.map(|title| title.inner_html())
}
}
Burada birkaç kritik nokta var:
- Dönüşte, 10. bölümde gördüğümüz
impl Traitsözdizimi kullanılıyor. - Dönen değer
Futureuygular veOutputtürüOption<String>olur. - Orijinal fonksiyon gövdesindeki bütün kod, bir
async moveblok içine sarılmıştır. - Blok ifadesi fonksiyonun gerçek dönüş değeridir.
Bir Async Fonksiyonu Çalışma Zamanıyla Yürütmek
İlk adım olarak tek bir sayfanın başlığını alalım. 17-3 numaralı liste bunu gösteriyor; ama bu hali henüz derlenmez.
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match sayfa_basligi(url).await {
Some(title) => println!("{url} için başlık {title} idi"),
None => println!("{url} için başlık yoktu"),
}
}
async fn sayfa_basligi(url: &str) -> Option<String> {
let yanit_metni = trpl::get(url).await.text().await;
Html::parse(&yanit_metni)
.select_first("title")
.map(|title| title.inner_html())
}
sayfa_basligi fonksiyonunu main içinden çağırmak- bölümde komut satırı argümanlarını alırken kullandığımız aynı deseni
izliyoruz. Sonra URL’yi
sayfa_basligifonksiyonuna verip sonucu bekliyoruz. SonuçOption<String>olduğu için, sayfanın başlığı olup olmamasına göre farklı mesajlar yazdırmak adınamatchkullanıyoruz.
Sorun şu: await anahtar sözcüğünü yalnızca async fonksiyonlarda ya da async
bloklarda kullanabilirsiniz. Rust, özel main fonksiyonunu doğrudan async
yapmanıza izin vermez.
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
mainin async olamamasının sebebi, async kodun bir çalışma zamanına
(runtime) ihtiyaç duymasıdır. Çalışma zamanı, eşzamansız kodun yürütülme
ayrıntılarını yöneten crate’tir. Bir programın main fonksiyonu çalışma
zamanını başlatabilir; ama onun kendisi çalışma zamanı değildir. Async kod
çalıştıran her Rust programı, future’ları yürütecek bir çalışma zamanı kurulan
en az bir noktaya sahiptir.
Async destekleyen birçok dil çalışma zamanını dilin içine gömer; Rust bunu yapmaz. Bunun yerine, hedef kullanım durumuna göre farklı ödünleşimler yapan çeşitli async çalışma zamanları vardır. Yüksek trafikli, çok çekirdekli bir sunucunun ihtiyaçlarıyla tek çekirdekli küçük bir mikrokontrolcünün ihtiyaçları aynı değildir.
Bu bölümde trpl crate’inden block_on fonksiyonunu kullanacağız. Bu
fonksiyon, bir future alır ve o future tamamlanana kadar mevcut iş parçacığını
bekletir. Arka planda tokio kullanarak bir çalışma zamanı kurar ve verdiğiniz
future’ı çalıştırır. Future bitince de onun ürettiği değeri geri döndürür.
İsterseniz sayfa_basligi’ndan dönen future’ı doğrudan block_ona verip sonuç
üzerinde match yapabilirsiniz. Ama çoğu gerçek async kodda tek bir async
çağrıdan fazlası olduğu için, biz 17-4 numaralı listedeki gibi bir async blok
geçip sayfa_basligi çağrısını onun içinde await edeceğiz.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let url = &args[1];
match sayfa_basligi(url).await {
Some(title) => println!("{url} için başlık {title} idi"),
None => println!("{url} için başlık yoktu"),
}
})
}
async fn sayfa_basligi(url: &str) -> Option<String> {
let yanit_metni = trpl::get(url).await.text().await;
Html::parse(&yanit_metni)
.select_first("title")
.map(|title| title.inner_html())
}
trpl::block_on ile bir async bloğu beklemekBu kodu çalıştırdığımızda başta beklediğimiz davranışı alırız:
$ cargo run -- "https://www.rust-lang.org"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/asenkron_bekleme 'https://www.rust-lang.org'`
https://www.rust-lang.org için başlık Rust Programming Language idi
Her await noktası, denetimin çalışma zamanına geri verildiği yerdir.
Çalışma zamanının daha sonra dönüp devam edebilmesi için, derleyici async blok
içindeki durumu görünmez bir durum makinesi olarak saklar. Sanki aşağıdaki gibi
bir enum yazmışsınız gibi düşünebilirsiniz:
extern crate trpl; // required for mdbook test
enum SayfaBasligiFuture<'a> {
Baslangic { url: &'a str },
GetBeklemeNoktasi { url: &'a str },
MetinBeklemeNoktasi { yanit: trpl::Response },
}
Bu durum makinesini elle yazmak yorucu ve hataya açık olurdu. Neyse ki Rust derleyicisi async kod için gereken veri yapılarını otomatik olarak üretip yönetir. Ödünç alma ve sahiplik kuralları da aynı şekilde geçerli olmaya devam eder.
Nihayetinde bu durum makinesini bir şeyin yürütmesi gerekir; işte o şey çalışma zamanıdır. Bu nedenle async dünyasında sık sık executor terimini de görürsünüz: executor, çalışma zamanının async kodu fiilen yürüten parçasıdır.
Artık 17-3’te neden doğrudan async fn main yazamadığımızı daha net görebiliriz.
main async olsaydı, ondan dönen future’ın durum makinesini de başka bir şeyin
yönetmesi gerekirdi. Oysa programın başlangıç noktası zaten maindir. Bu
yüzden main içinde trpl::block_on çağırıp çalışma zamanını elle kurduk.
Not: Bazı çalışma zamanları, doğrudan async
mainyazmanızı sağlayan makrolar sunar. Bu makrolar perde arkasında bizim 17-4’te elle yaptığımızı yapar: normal birmainoluşturur, içinde çalışma zamanını başlatır ve future’ı tamamlanana kadar yürütür.
İki URL’yi Eşzamanlı Olarak Yarıştırmak
Şimdi sayfa_basligi fonksiyonunu komut satırından aldığımız iki farklı URL ile
çağırıp hangisinin önce döndüğünü görelim. 17-5 numaralı liste bunu yapar.
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let baslik_gelecegi_1 = sayfa_basligi(&args[1]);
let baslik_gelecegi_2 = sayfa_basligi(&args[2]);
let (url, olasi_baslik) =
match trpl::select(baslik_gelecegi_1, baslik_gelecegi_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} ilk döndü");
match olasi_baslik {
Some(title) => println!("Sayfa başlığı şuydu: '{title}'"),
None => println!("Başlığı yoktu."),
}
})
}
async fn sayfa_basligi(url: &str) -> (&str, Option<String>) {
let yanit_metni = trpl::get(url).await.text().await;
let title = Html::parse(&yanit_metni)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
sayfa_basligi çağırıp hangisinin önce döndüğünü görmekÖnce iki URL için ayrı ayrı sayfa_basligi çağırıyor ve dönen future’ları
baslik_gelecegi_1 ile baslik_gelecegi_2 içinde saklıyoruz. Bunlar henüz
hiçbir şey yapmaz; çünkü future’lar tembeldir ve onları daha beklemedik.
Sonra bunları trpl::select fonksiyonuna veriyoruz. select, kendisine verilen
future’lardan hangisi önce tamamlansa ona göre bir değer döndürür.
trpl::select sonucunda Either::Left ya da Either::Right gelir. Hangi taraf
döndüyse, ona karşılık gelen URL ve başlık bilgisini alırız. Böylece “ilk önce
hangi URL döndü?” sorusuna cevap verip, eğer başlık varsa onu da yazdırabiliriz.
Bu örnek bize iki önemli şeyi gösterir:
- Future’lar ancak beklendiklerinde gerçekten yürür.
- Birden fazla future’ı aynı anda başlatmak için onları teker teker
awaitetmek yerine,selectveyajoingibi yardımcılarla birlikte yürütmek gerekir.
Böylece ilk gerçek async programımızı da tamamlamış olduk. Sonraki bölümde, aynı yaklaşımı daha genel eşzamanlılık problemlerine uygulayacağız.