Rust'ta Async/Await: Tokio, Futures ve Asenkron Eszamanlilik Rehberi

Rust'ta async/await ile asenkron programlama rehberi. Tokio runtime, Future trait, channel yapilari, semaphore ile hiz sinirlandirma, spawn_blocking ve hata yonetimi. Uretim ortamina yonelik kod ornekleri ve mulakat sorularina hazirlik.

Rust programlama dilinde Tokio, Futures ve asenkron eszamanlilik yapilarini gosteren teknik diyagram

Asenkron programlama, Rust dilinin en guclu ve ayni zamanda en incelikli ozelliklerinden birini olusturmaktadir. Bircok modern programlama dili async/await yapisini thread havuzlari veya event loop mekanizmalari uzerinde soyutlama olarak sunarken, Rust temel olarak farkli bir yol izlemektedir: Rust'ta Future yapilar sifir maliyetli soyutlamalardir (zero-cost abstractions), derleme zamaninda cozumlenir ve calisma zamaninda ek yuk olusturmaz. Ownership sistemi, borrow checker ve poll tabanli Future modeli bir araya geldiginde, Rust'un asenkron ekosistemi sistem programlama dilleri arasinda benzersiz bir konuma yerlesir.

Bu makalede Rust'ta async/await mekanizmasinin teknik temelleri ele alinmakta, Tokio runtime fiili standart olarak incelenmekte ve channel yapilari, semaphore ile hiz sinirlandirma, spawn_blocking gibi ileri duzey desenler ayrintili olarak islenmektedir. Tum kavramlar, hem teknik mulakatlarda hem de uretim kodunda dogrudan uygulanabilir pratik orneklerle desteklenmektedir.

Temel Kavram: Tembel (Lazy) Future Yapilari

JavaScript Promise yapilari olusturuldugu anda calistirilirken, Rust Future yapilari dogasi geregi tembeldir. Bir Future, aktif olarak poll edilmedigi surece hicbir kod calistirmaz. Yalnizca bir Future olusturmak hicbir hesaplamayi baslatmaz; .await cagrisi yapilmadan veya Tokio gibi bir runtime'a teslim edilmeden yurutme gerceklesmez. Bu davranis, Rust'taki asenkron mimarinin tamamini anlamanin temelini olusturmaktadir.

Rust Future Yapisinin Diger Dillerden Farki

Rust'ta Future modeli, standart kutuphanedeki Future trait'ine dayanmaktadir. Rust'taki her asenkron ifade bu trait'i uygulamaktadir ve trait, tek bir metot tanimlar: poll. Runtime, Future hazir bir deger dondurunceye kadar poll metodunu tekrar tekrar cagirmaktadir.

rust
// core::future::Future
trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Donus tipi olan Poll<Self::Output>, iki varyanta sahip bir enum'dur: Poll::Ready(value) Future'in tamamlandigini ve value degerini dondurdugunu belirtir. Poll::Pending ise Future'in henuz hazir olmadigini ve daha sonra yeniden poll edilmesi gerektigini ifade eder. Context parametresi bir Waker icerir; bu yapi, Future'in harici bir olay gerceklestiginde (ornegin bir ag isleminin tamamlanmasi) runtime'i yeniden poll etmesi gerektigine dair bilgilendirmesini saglar.

Bu mekanizma, JavaScript ve Python'dan temelden ayrilmaktadir. JavaScript'te bir Promise olusturuldugu anda baslatilir ve arka planda event loop uzerinde calisir. Python asyncio'da coroutine'ler olusturulduklarinda benzer sekilde planlanir. Rust Future yapilari ise bir runtime tarafindan acikca poll edilene kadar hicbir islem gerceklestirmez. Bu tasarim, gizli yan etkileri ortadan kaldirir ve derleyiciye Future'leri heap tahsisi veya dinamik dispatch gerektirmeyen durum makinelerine (state machine) optimize etme imkani tanir.

Pin<&mut Self> parametresi, Future poll edilirken bellekte tasinmamasini garanti eder. Bu gereklilik, derleyicinin async bloklari kendi alanlarindan referanslar icerebilen durum makinelerine donusturmesinden kaynaklanmaktadir. Pin olmadan, bu oz-referanslar (self-references) bellekte tasima sonrasinda gecersiz adreslere isaret edebilir.

Tokio Runtime Kurulumu ve Ilk Asenkron Fonksiyon

Rust, standart kutuphanesinde bir asenkron runtime sunmamaktadir. Bu tasarim bilingli olarak moduler tutulmustur: dil cekirdegi async, await ve Future trait'ini saglarken, fiili yurutme harici bir runtime'a devredilmektedir. Tokio, Rust uretim sistemlerinin buyuk cogulugunda kullanilan fiili standart runtime olarak one cikmaktadir.

Kurulum Cargo.toml dosyasi uzerinden gerceklestirilir:

toml
# Cargo.toml
[dependencies]
tokio = { version = "2", features = ["rt-multi-thread", "macros", "net", "time"] }

rt-multi-thread ozelligi, isi birden fazla isletim sistemi thread'ine dagitan cok-thread'li runtime'i aktive eder. macros, #[tokio::main] niteliginin kullanimini mumkun kilar ve bu nitelik main fonksiyonunu asenkron bir fonksiyona donusturur. net ve time ise asenkron ag ve zamanlayici islemlerini saglamaktadir.

Bu yapilandirmayla ilk asenkron Rust uygulamasi yazilabilir:

main.rsrust
#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("Got: {result}");
}

async fn fetch_data() -> String {
    // Simulate async I/O
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    String::from("data loaded")
}

#[tokio::main] makrosu arka planda senkron bir main fonksiyonu olusturur, bir Tokio runtime baslatir ve asenkron bloku bu runtime uzerinde calistirir. fetch_data() bir Future dondurur ve bu Future ancak .await cagrildiginda yurutulur. .await noktasi ayni zamanda bir yield noktasidir: zamanlayici calisirken runtime ayni thread uzerinde baska gorevleri calistirabilir.

Gorev Olusturma ve Yapilandirilmis Eszamanlilik

.await sirali asenkron yurutme saglarken, tokio::spawn bagimsiz gorevlerin paralel olarak calistirilmasini mumkun kilar. Her spawn edilen gorev, Tokio runtime uzerinde bagimsiz bir birim olarak calisir ve mevcut herhangi bir worker thread tarafindan yurutulebilir.

concurrent_tasks.rsrust
use tokio::task::JoinHandle;

#[tokio::main]
async fn main() {
    // Spawn two independent tasks
    let handle_a: JoinHandle<u32> = tokio::spawn(async {
        expensive_computation("dataset_a").await
    });

    let handle_b: JoinHandle<u32> = tokio::spawn(async {
        expensive_computation("dataset_b").await
    });

    // Await both results
    let (result_a, result_b) = (
        handle_a.await.expect("task A panicked"),
        handle_b.await.expect("task B panicked"),
    );

    println!("Results: {result_a}, {result_b}");
}

async fn expensive_computation(name: &str) -> u32 {
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    println!("{name} done");
    42
}

tokio::spawn, calisan goreve bir tutamac (handle) gorevi goren JoinHandle<T> dondurur. Bu handle uzerinde .await cagrildiginda, cagrici kod sonucu bekler. Her iki gorev runtime uzerinde esanli olarak calistigi icin, yukaridaki ornekte her hesaplama bir saniye surse de toplam calisma suresi yalnizca yaklasik bir saniyedir.

handle.await donus degeri Result<T, JoinError> tipindedir. JoinError, spawn edilen gorev panic yaptiginda veya runtime kapatildiginda ortaya cikar. Ornekteki .expect() kullanimi bu hatalardan birini panic'e donusturur; uretim kodunda bu kisim uygun hata yonetimiyle degistirilmelidir.

tokio::spawn icin Send + 'static Gereksinimleri

tokio::spawn'a verilen her Future, Send ve 'static trait'lerini karsilamalidir. Send gereklidir cunku Tokio gorevleri worker thread'ler arasinda tasiyabilir. 'static, Future'in sinirli yasam sureli referanslar icermemesi gerektigini ifade eder cunku gorev, cagricisindan daha uzun yasayabilir. Yerel degiskenlere referanslarin spawn edilen gorevlere aktarilmasi sik karsilasilan bir hataya yol acar. Cozum, verileri klonlamak veya async bloka tasinmadan once Arc ile sarmaktir.

tokio::join! ve tokio::select! ile Future Birlestirme

tokio::spawn disinda Tokio, acik gorev olusturma gerektirmeden yapilandirilmis eszamanlilik saglayan iki makro sunmaktadir: tokio::join! ve tokio::select!.

tokio::join! birden fazla Future'i ayni gorev uzerinde esanli olarak calistirir ve tumunun tamamlanmasini bekler. Birden fazla bagimsiz veri kaynaginin paralel olarak sorgulanmasi gereken senaryolar icin idealdir:

join_example.rsrust
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    // All three run concurrently, total time ~200ms (not 600ms)
    let (users, orders, inventory) = tokio::join!(
        fetch_users(),
        fetch_orders(),
        fetch_inventory()
    );

    println!("Users: {}, Orders: {}, Stock: {}", users.len(), orders.len(), inventory);
}

async fn fetch_users() -> Vec<String> {
    sleep(Duration::from_millis(200)).await;
    vec!["Alice".into(), "Bob".into()]
}

async fn fetch_orders() -> Vec<String> {
    sleep(Duration::from_millis(150)).await;
    vec!["ORD-001".into()]
}

async fn fetch_inventory() -> u32 {
    sleep(Duration::from_millis(100)).await;
    84
}

Uc Future da ayni gorev icinde calistigi icin toplam sure en yavas Future tarafindan belirlenir: sirali yurutmedeki 450 milisaniye yerine 200 milisaniye. tokio::spawn'dan farkli olarak join! icindeki Future'ler Send veya 'static gerektirmez; bu da yerel degiskenlere referanslarla kullanimi kolaylastirir.

tokio::select! ise ilk tamamlanan Future'i bekler ve digerlerini iptal eder. Zaman asimi desenleri, onbellek yedekleme mekanizmalari veya iptal mantigi icin kullanislidir:

select_example.rsrust
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    tokio::select! {
        val = fetch_from_cache() => {
            println!("Cache hit: {val}");
        }
        val = fetch_from_database() => {
            println!("DB result: {val}");
        }
    }
}

async fn fetch_from_cache() -> String {
    sleep(Duration::from_millis(5)).await;
    "cached_value".into()
}

async fn fetch_from_database() -> String {
    sleep(Duration::from_millis(50)).await;
    "db_value".into()
}

Bu ornekte onbellek erisimi 5 milisaniyede tamamlandigi icin kazanir; veritabani sorgusu 50 milisaniye gerektirmektedir. Veritabani erisimi otomatik olarak iptal edilir (drop edilir). select! Future'leri ayri thread'lerde calistirmaz; bunun yerine ayni gorev uzerinde sirali olarak poll eder ve ilk tamamlanani secer.

Rust mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Asenkron Rust Kodunda Hata Yonetimi

Asenkron Rust'ta hata yonetimi, senkron koddakiyle ayni ilkeleri takip eder: Result<T, E> ve ? operatoru. Asenkron islemler genellikle farkli hata kaynaklariyla (ag, deserialization, dosya sistemi) etkilestigi icin, ozel bir hata enum'u tanimlamak onerilen yaklasimdir.

error_handling.rsrust
use std::io;

#[derive(Debug)]
enum AppError {
    Network(reqwest::Error),
    Parse(serde_json::Error),
    Io(io::Error),
}

async fn load_config(url: &str) -> Result<Config, AppError> {
    let response = reqwest::get(url)
        .await
        .map_err(AppError::Network)?;

    let text = response.text()
        .await
        .map_err(AppError::Network)?;

    let config: Config = serde_json::from_str(&text)
        .map_err(AppError::Parse)?;

    Ok(config)
}

#[derive(serde::Deserialize)]
struct Config {
    db_url: String,
    port: u16,
}

.map_err(AppError::Variant)? deseni, belirli hata tipini kendi hata varyantina donusturur ve ust katmana iletir. Bu yaklasim Rust'ta idiomatiktir ve senkron ile asenkron kodda ayni sekilde calisir. ? operatoru mevcut fonksiyonu erken sonlandirir ve hatayi dondurur; bu sayede ic ice match ifadeleri onlenir.

Daha buyuk projelerde thiserror crate'i otomatik From uygulamalari saglar ve acik map_err olmadan ? operatorunun kullanilmasina olanak tanir. Prototiplerde ve betiklerde ise her turlu hatayi kabul eden evrensel bir hata tipi olarak anyhow::Result tercih edilebilir.

Asenkron Iletisim icin Channel Yapilari

Gorevlerin birbirleriyle iletisim kurmasi gerektiginde, Tokio channel yapilari guvenli ve verimli bir cozum sunmaktadir. mpsc channel (Multiple Producer, Single Consumer), asenkron gorevler arasindaki iletisim icin en yaygin kullanilan desendir.

channel_example.rsrust
use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    // Bounded channel with capacity 32
    let (tx, mut rx) = mpsc::channel::<String>(32);

    // Producer task
    let producer = tokio::spawn(async move {
        for i in 0..5 {
            tx.send(format!("message-{i}")).await.unwrap();
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
        }
        // tx dropped here, closing the channel
    });

    // Consumer reads until channel closes
    while let Some(msg) = rx.recv().await {
        println!("Received: {msg}");
    }

    producer.await.unwrap();
}

mpsc::channel 32 kapasiteyle olusturulmaktadir; bu, gondericinin bloke olmasindan once tamponda 32 mesajin bekleyebilecegi anlamina gelir. Gonderici (tx) drop edildiginde, aliciya (rx) baska mesaj gelmeyecegini bildirir ve rx.recv() None dondurur. Bu desen, tuketici dongusunun temiz bir sekilde sonlandirilmasini saglar.

Tokio, mpsc disinda tek bir mesaj icin oneshot, birden fazla tuketici icin broadcast ve en guncel degerin dagitimi icin watch channel tiplerini de sunmaktadir. Channel tipinin secimi, iletisim desenine dogrudan bagli olup performans ve bellek tuketimini etkiler.

Gercek Dunya Deseni: Semaphore ile Hiz Sinirli Eszamanli HTTP

Pratik uygulamalarda asenkron sistemlerin genellikle esanli islem sayisini sinirlamasi gerekmektedir; gerek harici API'lerin asiri yuklenmesini onlemek gerekse veritabani baglantilari gibi kaynaklari korumak icin. Tokio'nun Semaphore yapisi bu sorun icin zarif bir cozum sunmaktadir.

rate_limited_fetcher.rsrust
use std::sync::Arc;
use tokio::sync::Semaphore;

async fn fetch_all(urls: Vec<String>, max_concurrent: usize) -> Vec<Result<String, String>> {
    let semaphore = Arc::new(Semaphore::new(max_concurrent));
    let mut handles = Vec::new();

    for url in urls {
        let sem = Arc::clone(&semaphore);
        let handle = tokio::spawn(async move {
            // Acquire permit before making request
            let _permit = sem.acquire().await.unwrap();
            reqwest::get(&url)
                .await
                .map(|r| r.status().to_string())
                .map_err(|e| e.to_string())
            // permit dropped here, allowing next task to proceed
        });
        handles.push(handle);
    }

    let mut results = Vec::new();
    for handle in handles {
        results.push(handle.await.unwrap());
    }
    results
}

Semaphore, max_concurrent sayida izin (permit) ile baslatilir. Her gorev, asil istegi gerceklestirmeden once bir izin edinmelidir. Tum izinler dagitildiginda, sem.acquire().await baska bir gorev iznini serbest birakana kadar asenkron olarak bekler. Izin, _permit degiskeni kapsam disindan ciktiginda otomatik olarak serbest birakilir; bu durum Rust'un RAII mekanizmasi tarafindan garanti edilmektedir.

Bu desen; web scraping, API toplama ve binlerce istegin esanli olarak baslatilabilecegi ancak yalnizca sinirli sayidanin aktif olmasi gereken toplu islemler icin ozellikle faydalidir.

Eszamanlilik Ilkeli Secimi

join!, spawn ve semaphore arasindaki secim, kullanim senaryosuna baglidir. tokio::join!, birlikte tamamlanmasi gereken bilinen, az sayida Future icin uygundur. tokio::spawn, cok-thread'li yurutmeden fayda saglayan bagimsiz gorevler icin dogru secimdir. Semaphore'lar spawn ile eszamanlilik ust sinirini birlestirir. Basit zaman asimi mantigi veya onbellek yedekleme mekanizmalari icin tokio::select! uygun semantigi saglar. Bu ilkellerin kombinasyonu, karmasik asenkron mimarilerin hassas bir sekilde modellenmesini mumkun kilmaktadir.

Pin ve Unpin: Future Yapilari icin Bellek Guvenligi

Pin ve Unpin tipleri, asenkron Rust ekosisteminde en sik yanlis anlasilan kavramlar arasinda yer almaktadir. Amaci, derleyicinin async bloklari durum makinelerine donustururken olusturdugu oz-referansli yapilanin (self-referential struct) bellek guvenligini garanti etmektir.

Bir async blok, yerel bir degiskene referansi bir .await noktasi uzerinden tasiyorsa, derleyici hem degiskeni hem de ona olan referansi alan olarak iceren bir durum makinesi olusturur. Bu durum makinesi bellekte tasinirsa, referans gecersiz bir adrese isaret eder. Pin<&mut Self> tam olarak bunu onler: sabitlenmis (pinned) bir deger artik tasinamaz.

Cogu gelistirici Pin ile dogrudan etkilesime girmez cunku derleyici ve .await sozdizimi ayrintilari soyutlar. Acik sabitlemenin (explicit pinning) gerekli hale geldigi durumlar sunlardir: Future trait'inin manuel uygulanmasi, dinamik dispatch edilen Future'ler icin Box::pin kullanimi ve select! ifadelerinde pin! makrolarinin kullanimi. Unpin uygulayan tipler sabitlenmis olsalar bile serbestce tasinabilir. Rust'taki standart tiplerin cogu Unpin'i otomatik olarak uygulamaktadir.

Performans Ozellikleri ve spawn_blocking

Tokio'nun cok-thread'li runtime'i varsayilan olarak makinedeki CPU cekirdegi sayisi kadar worker thread kullanir. Her worker thread kendi event loop'unu isletir ve yuku esit dagitmak icin diger thread'lerden gorev calabilir (work-stealing). Asenkron gorevler, worker thread'ler uzerinde uzun sureli CPU-yogun hesaplamalar yapmamadir cunku bu, ayni thread'deki diger gorevleri bloke edecektir.

CPU-yogun islemler icin Tokio, kodu ozellikle bloke eden islemler icin ayrilan ayri bir thread havuzunda calistiran spawn_blocking sunmaktadir:

spawn_blocking_example.rsrust
#[tokio::main]
async fn main() {
    let hash = tokio::task::spawn_blocking(|| {
        // CPU-intensive work runs on a blocking thread
        compute_hash(b"large dataset")
    })
    .await
    .unwrap();

    println!("Hash: {hash}");
}

fn compute_hash(data: &[u8]) -> String {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    let mut hasher = DefaultHasher::new();
    data.hash(&mut hasher);
    format!("{:x}", hasher.finish())
}

spawn_blocking, verilen closure'u varsayilan olarak 512 thread'e kadar genisleyebilen ozel bir bloke eden thread havuzuna tasir. Donus degeri, asenkron olarak beklenebilen bir JoinHandle'dir ve worker thread'leri bloke etmez. Bu desen; parola hashleme, goruntu isleme, sikistirma veya birkac mikrosaniyeden uzun suren herhangi bir hesaplama icin zorunludur.

Temel kural su sekildedir: 10-100 mikrosaniyeden fazla CPU zamani gerektiren her islem spawn_blocking icinde calistirilmalidir. Tokio'nun asenkron alternatifleri (tokio::fs) uzerinden gerceklestirilmeyen senkron dosya sistemi islemleri de ayni sekilde spawn_blocking icinde yer almalidir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Sonuc

Rust'ta asenkron programlama, en yuksek performansi tip sisteminin guvenlik garantileriyle birlestirmektedir. Temel cikarimlar asagidaki sekilde ozetlenebilir:

  • Rust Future yapilari tembel (lazy) olup yalnizca poll edildiginde kod calistirir; bu ozellik onlari JavaScript Promise ve Python coroutine yapilarindan temelden ayirir
  • Future trait'i ve poll metodu, tum rust async await ekosisteminin temelini olusturur ve heap tahsisi gerektirmeyen sifir maliyetli soyutlamalari mumkun kilar
  • Tokio, asenkron Rust icin standart runtime olup cok-thread'li zamanlama, zamanlayicilar, ag I/O ve senkronizasyon ilkelleri saglar
  • tokio::spawn, Send + 'static gereksinimleriyle bagimsiz gorevler olustururken, tokio::join! bu kisitlamalar olmaksizin birden fazla Future'i ayni gorev icinde calistirir
  • tokio::select! ilk tamamlanan Future'i bekler ve digerlerini iptal eder; zaman asimi ve yedekleme desenleri icin idealdir
  • Hata yonetimi, senkron Rust koduyla ayni sekilde Result<T, E> ve ? operatorunu kullanir; farkli hata kaynaklarinin toplanmasi icin ozel hata enum'lari tavsiye edilir
  • Channel yapilari (mpsc, oneshot, broadcast, watch) gorevler arasinda farkli semantiklerle guvenli iletisim saglar
  • Semaphore'lar eszamanliligi sinirlandirarak kontrolllu izin dagitimi ile harici kaynaklarin asiri yuklenmesini onler
  • Pin, oz-referansli Future'lerin bellek guvenligini garanti eder ve cogu durumda derleyici tarafindan otomatik olarak yonetilir
  • spawn_blocking, CPU-yogun veya senkron bloke eden islemleri asenkron worker thread'leri engellemeden ayri bir thread havuzuna aktarir
  • Bu kavramlarin anlasilmasi hem rust concurrency interview icin hem de yuksek performansli uretim sistemlerinin gelistirilmesi icin vazgecilmezdir

Etiketler

#rust
#async
#tokio
#futures
#concurrency

Paylaş

İlgili makaleler