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

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.

Filename: src/main.rs
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())
}
Listing 17-1: HTML sayfasından başlık etiketini almak için async fonksiyon tanımlamak

Ö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::spawn ile 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.

Filename: src/main.rs
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())
}
Listing 17-2: await anahtar sözcüğüyle zincirleme çağrı yapmak

Bö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 Trait sözdizimi kullanılıyor.
  • Dönen değer Future uygular ve Output türü Option<String> olur.
  • Orijinal fonksiyon gövdesindeki bütün kod, bir async move blok 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.

Filename: src/main.rs
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())
}
Listing 17-3: Kullanıcının verdiği argümanla sayfa_basligi fonksiyonunu main içinden çağırmak
  1. bölümde komut satırı argümanlarını alırken kullandığımız aynı deseni izliyoruz. Sonra URL’yi sayfa_basligi fonksiyonuna verip sonucu bekliyoruz. Sonuç Option<String> olduğu için, sayfanın başlığı olup olmamasına göre farklı mesajlar yazdırmak adına match kullanı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.

Filename: src/main.rs
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())
}
Listing 17-4: trpl::block_on ile bir async bloğu beklemek

Bu 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 main yazmanızı sağlayan makrolar sunar. Bu makrolar perde arkasında bizim 17-4’te elle yaptığımızı yapar: normal bir main oluş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.

Filename: src/main.rs
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)
}
Listing 17-5: İki URL için 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 await etmek yerine, select veya join gibi 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.