Rust'ta async fn ya da async { ... } bloğu yazdığınızda kod hemen çalışmaz; derleyici onu Future trait'ini uygulayan anonim bir değere çevirir. Bu future'ın sonucunu almak için başka bir async bağlamın içinde .await kullanırsınız. Zincirin en tepesinde ise future'ları sürekli yoklayan (poll) bir yürütücü (executor) bulunmalıdır. Standart kütüphane Future trait'ini, async ve await söz dizimini ve Waker gibi temel parçaları verir, ama hazır bir çalışma zamanı (runtime) içermez; bu yüzden main fonksiyonu async olamaz ve bir async işi çalıştırmak için ya bir runtime crate'i ya da kendi küçük yürütücünüz gerekir.
Bu yazıda async kodun neye dönüştüğünü, future'ların neden tembel olduğunu, Future::poll ve Waker mekanizmasını ve std belgesindeki Wake örneğinden uyarlanan, yalnızca standart kütüphaneyle yazılmış bir block_on fonksiyonunu anlatıyoruz. The Rust Book'un async bölümü örneklerinde üçüncü taraf crate'leri saran yardımcı bir crate kullanır; biz bunun yerine her örneği std ile, Rust 1.99.1 ve edition 2024 altında derleyip çalıştırdık. İş parçacığı tabanlı eşzamanlılık için Rust thread::spawn, seri sırası için Rust öğrenme rehberi sayfasına bakabilirsiniz.
Eşzamanlılık, paralellik ve async ne çözer?

Kitap iki kavramı bir yazılım ekibi benzetmesiyle ayırır. Bir kişinin birkaç işi sırayla değiştirerek ilerletmesi eşzamanlılıktır (concurrency); her ekip üyesinin kendi işini aynı anda yapması paralelliktir (parallelism). Tek çekirdekli bir makine yalnızca eşzamanlı çalışabilir, çok çekirdekli makine paralel de çalışabilir. Kitaba göre Rust'ta async kod genellikle eşzamanlı çalışır; donanıma, işletim sistemine ve kullanılan runtime'a bağlı olarak arka planda paralellik de kullanılabilir.
Kitap işleri ikiye ayırır: video dışa aktarma gibi işlemci gücüyle sınırlı olan CPU ağırlıklı (CPU-bound) işler ve ağdan veri indirmek gibi giriş çıkış hızıyla sınırlı olan G/Ç ağırlıklı (I/O-bound) işler. Her indirme için ayrı bir iş parçacığı açmak çalışır, ama iş parçacığı sayısı arttıkça sistem kaynakları sorun olur. Async modeli, çalışma noktalarını ve olası duraklama noktalarını ifade etmenize, işleri hangi sırayla ve nasıl yürüteceğine ise runtime'ın karar vermesine izin verir.
async fn, async blok ve Future trait
Kitabın anlattığı dönüşüm şudur: derleyici async ile işaretlenmiş bir bloğu, Future trait'ini uygulayan benzersiz ve anonim bir veri tipine çevirir. async ile işaretlenmiş bir fonksiyonu ise gövdesi bir async blok olan sıradan bir fonksiyona çevirir. Yani async fn f(x: &str) -> usize kabaca fn f(x: &str) -> impl Future<Output = usize> imzalı ve gövdesi async move { ... } olan bir fonksiyona eşdeğerdir. Reference'a göre bu async move blok, kullanılmayanlar dahil tüm parametreleri yakalar ve parametreler future tamamen beklendiğinde düşürülür. impl Trait dönüş tipleri için Rust trait yazısına bakabilirsiniz.
Trait'in kendisi kısadır: bir Output ilişkili tipi ve fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> metodu. Poll iki varyantlı bir enum'dur: Ready(T) işin bittiğini ve değerin hazır olduğunu, Pending ise daha sonra yeniden sorulması gerektiğini söyler. Kitap Output tipini, iterator'daki Item tipine benzetir; iterator'ların next çağrılmadan çalışmaması da aşağıdaki tembellik kuralının yakın akrabasıdır (Rust iterator).
Her .await noktası denetimin runtime'a geri verilebileceği bir yerdir. Bunu mümkün kılmak için derleyici, Reference'ın ifadesiyle kabaca her await noktası için bir varyantı olan ve o noktadan devam etmek için gereken veriyi saklayan bir enum'a benzeyen bir durum makinesi üretir. Bu durum makinesini elle yazmanız gerekmez; sahiplik ve ödünç alma kuralları da onun için aynen denetlenir.
Future tembeldir: await yoksa hiçbir şey olmaz
Kitabın vurguladığı gibi Rust'ta future'lar siz await edene ya da poll edene kadar hiçbir şey yapmaz. Bu, thread::spawn davranışından farklıdır: oraya verdiğiniz closure hemen çalışmaya başlar. Kullanılmayan bir future için derleyici uyarı da verir:
async fn selamla(ad: &str) -> String {
println!(" gövde çalışıyor: {ad}");
format!("Merhaba {ad}")
}
fn main() {
selamla("Deniz"); // await yok: hiçbir şey çalışmaz, derleyici uyarır
}
// warning: unused implementer of `Future` that must be usedUyarının notu durumu tek cümleyle özetler: future'lar .await edilmedikçe ya da poll edilmedikçe hiçbir şey yapmaz. Program bu haliyle çalıştırıldığında ekrana hiçbir şey yazmaz, çünkü selamla gövdesi hiç çalışmaz. Bu tembellik, Kitaba göre Rust'ın iterator'larda olduğu gibi performans garantilerini koruyabilmesi için önemlidir.
main neden async olamaz?

await yalnızca async fonksiyonların ve async blokların içinde kullanılabilir. Peki en dıştaki future'ı kim çalıştıracak? main fonksiyonunu async yapmayı denerseniz derleyici izin vermez:
async fn main() {
println!("merhaba");
}
// error[E0752]: `main` function is not allowed to be `async`Kitap gerekçeyi şöyle açıklar: async kod bir runtime'a ihtiyaç duyar ve main bir runtime'ı başlatabilir, ama kendisi bir runtime değildir. Async destekleyen dillerin çoğu bir runtime ile gelir, Rust ise gelmez; çok çekirdekli bir web sunucusunun ihtiyaçlarıyla tek çekirdekli ve yığın belleği (heap) olmayan bir mikrodenetleyicinin ihtiyaçları çok farklı olduğu için farklı ödünleşimler yapan birçok runtime crate'i vardır. Bazı runtime'lar async fn main yazmanıza izin veren makrolar sunar; Kitaba göre bu makrolar kodu, future'ı sonuna kadar çalıştıran bir fonksiyonu çağıran sıradan bir main fonksiyonuna dönüştürür. Bu crate'ler std belgelerinin kapsamı dışındadır; incelemek isterseniz belgelerini docs.rs üzerinde bulabilirsiniz.
Yalnızca std ile bir block_on yürütücüsü
std belgesindeki Wake trait'i, bir görevi uyandırma mantığını Arc içinde tutan yürütücüler için güvenli bir yol sunar. Aynı sayfadaki örnek, bir future'ı mevcut iş parçacığında sonuna kadar çalıştıran basit bir block_on fonksiyonudur. Aşağıda bu örneği iki async fonksiyonla birlikte kullanıyoruz.
use std::pin::pin;
use std::sync::Arc;
use std::task::{Context, Poll, Wake};
use std::thread::{self, Thread};
// Rust 2024 edition'da Future prelude'da olduğu için ayrıca içe aktarmıyoruz.
// std belgesindeki Wake örneğinden uyarlanan en basit yürütücü (executor).
struct ThreadWaker(Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.0.unpark(); // uyuyan iş parçacığını uyandır
}
}
fn block_on<T>(fut: impl Future<Output = T>) -> T {
let mut fut = pin!(fut);
let waker = Arc::new(ThreadWaker(thread::current())).into();
let mut cx = Context::from_waker(&waker);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(sonuc) => return sonuc,
Poll::Pending => thread::park(), // wake gelene kadar bekle
}
}
}
async fn fiyat_getir(urun: &str) -> u32 {
match urun {
"kalem" => 15,
"defter" => 40,
_ => 0,
}
}
async fn sepet_toplami(urunler: &[&str]) -> u32 {
let mut toplam = 0;
for u in urunler {
toplam += fiyat_getir(u).await; // her await bir olası duraklama noktası
}
toplam
}
fn main() {
let toplam = block_on(sepet_toplami(&["kalem", "defter", "kalem"]));
println!("sepet: {toplam} TL");
}
// çıktı: sepet: 70 TLParçalar şöyle çalışır. pin! makrosu future'ı yerinde sabitler, çünkü poll metodu Pin<&mut Self> ister. ThreadWaker, mevcut iş parçacığının Thread tutamacını saklar ve uyandırıldığında unpark çağırır. Arc::new(...).into() bu yapıdan bir Waker, Context::from_waker da poll'a verilecek bağlamı üretir. Döngü future'ı poll eder; Ready gelirse sonucu döndürür, Pending gelirse thread::park() ile uyandırılana kadar bekler. Belge bu örneğin doğruluktan sadelik uğruna ödün verdiğini açıkça belirtir: üretim kalitesindeki yürütücüler araya giren unpark çağrılarını ve iç içe çağrıları da ele almalıdır. Yani bu fonksiyon öğrenmek ve küçük araçlar için uygundur, gerçek bir runtime'ın yerini tutmaz.
Örnekteki fiyat_getir hiç beklemediği için her .await ilk poll'da Ready döner ve döngü tek turda biter. Edition 2024 ile ilgili bir ayrıntı da kodda görülüyor: Edition Guide: Rust 2024 prelude değişikliği sayfasına göre Future ve IntoFuture trait'leri Rust 2024 prelude'una eklendi, bu yüzden use std::future::Future; satırına gerek kalmadı. Belge bunun, kendi trait'inizde poll adlı bir metot varsa çağrıyı belirsiz hale getirebileceği konusunda da uyarır.
Elle Future yazmak: poll, Pending ve wake
Async söz diziminin altında ne olduğunu görmenin en iyi yolu bir future'ı elle uygulamaktır. Aşağıdaki GeriSayim ilk iki poll'da Pending, üçüncüsünde Ready döner. Aynı programda async fonksiyonun gövdesinin ancak block_on çağrıldığında çalıştığını da görüyorsunuz.
use std::pin::{Pin, pin};
use std::sync::Arc;
use std::task::{Context, Poll, Wake};
use std::thread::{self, Thread};
struct ThreadWaker(Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
}
fn block_on<T>(fut: impl Future<Output = T>) -> T {
let mut fut = pin!(fut);
let waker = Arc::new(ThreadWaker(thread::current())).into();
let mut cx = Context::from_waker(&waker);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => thread::park(),
}
}
}
// Elle yazılmış bir future: hazır olana kadar iki kez Pending döner.
struct GeriSayim {
kalan: u32,
}
impl Future for GeriSayim {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.kalan == 0 {
println!(" poll: hazır");
Poll::Ready("bitti")
} else {
println!(" poll: kalan {}", self.kalan);
self.kalan -= 1;
cx.waker().wake_by_ref(); // "beni yeniden poll et" sinyali
Poll::Pending
}
}
}
async fn selamla(ad: &str) -> String {
println!(" gövde çalışıyor: {ad}");
format!("Merhaba {ad}")
}
fn main() {
let f = selamla("Deniz"); // yalnızca future oluşturuldu
println!("future oluşturuldu, henüz çalışmadı");
println!("{}", block_on(f));
println!("{}", block_on(GeriSayim { kalan: 2 }));
}
// çıktı: future oluşturuldu, henüz çalışmadı
// çıktı: gövde çalışıyor: Deniz
// çıktı: Merhaba Deniz
// çıktı: poll: kalan 2
// çıktı: poll: kalan 1
// çıktı: poll: hazır
// çıktı: bittiBurada kuralı kavramak önemlidir: Future belgesine göre Pending döndüren bir future, ilerleme mümkün olduğunda uyandırılmayı ayarlamaktan sorumludur; genellikle bağlamdaki Waker değerini klonlayıp saklar ve olay gerçekleşince wake çağırır. GeriSayim beklediği bir olay olmadığı için hemen wake_by_ref çağırıyor. Bu satırı silerseniz yürütücümüz park içinde sonsuza kadar bekler, çünkü onu uyandıracak kimse kalmaz. Belge ayrıca poll metodunun sıkı bir döngüde tekrar tekrar çağrılmaması, yalnızca future ilerleyebileceğini bildirdiğinde çağrılması gerektiğini; Ready döndükten sonra yeniden poll etmenin panik, sonsuz bekleme ya da başka sorunlara yol açabileceğini söyler.
Gerçek bekleme: başka bir iş parçacığından wake
Pratikte Pending, bir sonucun başka bir yerde hazırlanmasını beklemek anlamına gelir: bir soket okunabilir olacaktır, bir zamanlayıcı dolacaktır ya da başka bir iş parçacığı hesabını bitirecektir. Aşağıdaki örnekte future, işi arka plan iş parçacığına veriyor; ilk poll'da sonuç yoksa Waker değerini ortak duruma kaydedip Pending dönüyor. İş parçacığı işini bitirince sonucu yazıp bu waker'ı çağırıyor ve block_on uyanıp future'ı yeniden poll ediyor. Aynı örnek async bloklarda return ve ? operatörünün davranışını da gösteriyor.
use std::pin::{Pin, pin};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Wake, Waker};
use std::thread::{self, Thread};
use std::time::Duration;
struct ThreadWaker(Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
}
fn block_on<T>(fut: impl Future<Output = T>) -> T {
let mut fut = pin!(fut);
let waker = Arc::new(ThreadWaker(thread::current())).into();
let mut cx = Context::from_waker(&waker);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => return v,
Poll::Pending => thread::park(),
}
}
}
// Bir arka plan iş parçacığı işi bitirince waker'ı çağıran future.
#[derive(Default)]
struct Durum {
sonuc: Option<u64>,
waker: Option<Waker>,
}
struct ArkaPlanIsi {
durum: Arc<Mutex<Durum>>,
}
impl ArkaPlanIsi {
fn baslat(n: u64) -> Self {
let durum = Arc::new(Mutex::new(Durum::default()));
let d2 = Arc::clone(&durum);
thread::spawn(move || {
thread::sleep(Duration::from_millis(50)); // uzun süren iş
let mut d = d2.lock().unwrap();
d.sonuc = Some((1..=n).product());
if let Some(w) = d.waker.take() {
w.wake(); // future'ı bekleyen yürütücüyü uyandır
}
});
ArkaPlanIsi { durum }
}
}
impl Future for ArkaPlanIsi {
type Output = u64;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u64> {
let mut d = self.durum.lock().unwrap();
match d.sonuc {
Some(v) => Poll::Ready(v),
None => {
d.waker = Some(cx.waker().clone()); // en son waker'ı sakla
Poll::Pending
}
}
}
}
async fn hesapla() -> Result<u64, String> {
let a = ArkaPlanIsi::baslat(5).await;
let b = ArkaPlanIsi::baslat(3).await;
if b == 0 {
return Err("sıfıra bölme".into()); // async fn'in döndürdüğü future'ın sonucu olur
}
Ok(a / b)
}
fn main() {
println!("{:?}", block_on(hesapla()));
let blok = async {
let n: u32 = "12x".parse().map_err(|_| "sayı değil")?; // ? yalnızca bloğun sonucunu etkiler
Ok::<u32, &str>(n)
};
println!("{:?}", block_on(blok));
println!("main devam ediyor");
}
// çıktı: Ok(20)
// çıktı: Err("sayı değil")
// çıktı: main devam ediyorFuture belgesindeki bir not örneğin poll metoduna yansıdı: birden çok poll çağrısında yalnızca en son poll'a verilen bağlamdaki waker uyandırılmalıdır; bu yüzden her Pending dönüşünde waker'ı yeniden saklıyoruz. Ortak durum için kullandığımız Arc<Mutex<...>> birleşimi Rust Arc ve Mutex yazısında anlatılıyor. Belge, poll metodunun hızlı dönmesi ve bloklamaması gerektiğini, uzun sürecek işlerin bir iş parçacığı havuzuna devredilmesini önerir; örnekteki thread::sleep bu yüzden future'ın içinde değil, ayrı iş parçacığında.
async anahtar kelimesinin belgesi ve Reference aynı kuralı açıklar: async bloklar bir fonksiyon sınırı gibi davranır. İçlerindeki return ve ? çevreleyen fonksiyonu değil, bloğun ürettiği future'ın sonucunu belirler. Örnekte parse hatası main fonksiyonunu sonlandırmıyor; future Err("sayı değil") ile tamamlanıyor ve program devam ediyor. Aynı nedenle async blok içinden dıştaki bir döngüyü etkileyen break ya da continue yazamazsınız; Reference bunun E0267 hatası verdiğini gösterir. Bloğun hata tipini derleyici çıkaramadığında örnekteki Ok::<u32, &str> gibi açık bir tip vermeniz gerekir. ? ve Result için Rust Result ve ? operatörü yazısına bakın.
Pin ve Unpin kısaca
poll imzasındaki Pin, değerin bellekte taşınmayacağını garanti eden bir sarmalayıcıdır. Kitabın açıklamasına göre async blokların ürettiği durum makineleri kendi içlerine referans tutabilir; böyle bir değer bellekte taşınırsa referans geçersizleşir. Pin bunu engeller. Unpin trait'i ise taşınmasında sakınca olmayan tipleri işaretler ve çoğu tip otomatik olarak Unpin uygular. Bir future'ı doğrudan .await ettiğinizde sabitleme örtük olarak yapılır; bu yüzden günlük kodda Pin ile nadiren karşılaşırsınız. Kitaba göre bu konular daha çok alt seviye kütüphane ya da runtime yazarken önemlidir; karşılaştığınız tipik durum, farklı future'ları Vec<Box<dyn Future<Output = ()>>> gibi bir koleksiyona koymak istediğinizde alınan cannot be unpinned hatasıdır ve çözümü pin! ya da Box::pin kullanmaktır. Trait nesneleri için Rust dyn trait yazısına bakın.
Thread mi async mı?

Kitap bu soruya "duruma göre" der ve çoğu zaman cevabın ikisi birden olduğunu ekler. İş parçacıkları işletim sistemi tarafından yönetilir, her biri belirli bir bellek bedeli taşır ve işletim sistemi olmayan bazı gömülü sistemlerde hiç bulunmaz. Async modelde ise eşzamanlı işlemler kendi iş parçacıklarına ihtiyaç duymaz; runtime tarafından yönetilen görevler (task) üzerinde çalışır. Kitabın pratik kuralları şunlardır:
| Durum | Kitabın önerisi | Neden |
|---|---|---|
| Çok paralelleştirilebilir, CPU ağırlıklı iş | İş parçacıkları | Her parça ayrı çekirdekte bağımsız işlenebilir |
| Çok eşzamanlı, G/Ç ağırlıklı iş | Async | Farklı hızlarda gelen birçok kaynağı az kaynakla bekler |
| İkisi birden | Birlikte kullanın | Örneğin hesaplama ayrı iş parçacığında, bildirim async kanalla |
Kitap ayrıca birçok runtime'ın görevleri iş parçacıkları arasında taşıyan iş çalma (work stealing) yaklaşımını kullandığını belirtir; yani gerçek dünyada async ve iş parçacıkları çoğu zaman aynı sistemde yan yana bulunur. Bizim yukarıdaki son örneğimiz de bunun küçük bir modelidir: hesaplama bir iş parçacığında, bekleme bir future içinde yapılıyor.
Söz dizimiyle ilgili iki not daha: async ve await belgelerine göre bu anahtar kelimeler 2018 edition'dan itibaren vardır ve kararlı Rust'ta 1.39 sürümünden beri kullanılabilir. Reference ayrıca async |x| ... biçimindeki async closure'ları ve bunları parametre olarak almak için kullanılan AsyncFn trait ailesini tanımlar; çağrıldığında closure gövdesini çalıştırmaz, bir future üretir. Closure'ların genel davranışı için Rust closure yazısına bakın.
Sık yapılan hatalar
- Future'ı await etmeyi unutmak: fonksiyon çağrılmış gibi görünür ama gövde hiç çalışmaz. Derleyicinin unused_must_use uyarısını ciddiye alın.
- Async kod içinde bloklayan çağrı yapmak:
thread::sleep, bloklayan dosya ve ağ işlemleri ya daMutexkilidini bir await noktası boyunca tutmak yürütücüyü durdurur. Belge uzun işlerin bir iş parçacığı havuzuna devredilmesini önerir. - Pending dönüp wake ayarlamamak: elle yazılan future'da waker saklanmaz ya da çağrılmazsa görev bir daha poll edilmez ve program asılı kalır.
- Ready sonrası yeniden poll etmek: Future trait'i bu durumda herhangi bir davranış garanti etmez.
- Async bloktaki ? operatörünün fonksiyondan çıkacağını sanmak: ? yalnızca bloğun future'ını sonlandırır.
- Belgedeki block_on'u üretimde runtime yerine kullanmak: belge örneğin sadelik için doğruluktan ödün verdiğini açıkça söyler.
Sık Sorulan Sorular
Rust async için hangi runtime'ı kullanmalıyım?
Standart kütüphane bir runtime içermez ve resmî belgeler belirli bir crate önermez; Kitap örneklerinde kendi yardımcı crate'ini kullanır ve farklı ihtiyaçlar için farklı runtime'ların bulunduğunu söyler. Seçeneklerin belgelerine docs.rs üzerinden ulaşabilirsiniz; bu seri yalnızca std kapsamında kalıyor.
async fn çağırınca yeni bir iş parçacığı mı açılır?
Hayır. Çağrı yalnızca bir future değeri üretir. Bu future'ın hangi iş parçacığında ve ne zaman çalışacağına onu poll eden yürütücü karar verir; bizim block_on örneğimizde her şey çağıran iş parçacığında çalışıyor.
await ile thread join arasındaki fark nedir?
join çağıran iş parçacığını işletim sistemi seviyesinde bloklar. .await ise mevcut async bağlamı askıya alır ve denetimi yürütücüye bırakır; yürütücü bu sırada başka future'ları ilerletebilir. Reference'a göre await ifadesi yalnızca async bağlamda yazılabilir.
std::future::join! makrosunu kullanabilir miyim?
Bu sürümün belgelerinde std::future::join! deneysel (nightly-only) olarak işaretli; kararlı Rust'ta birden çok future'ı aynı anda beklemek için bir runtime crate'inin sağladığı araçlar kullanılır.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Rust öğrenme rehberi · Rust thread::spawn · Rust Arc ve Mutex · Rust mpsc kanalları · Rust trait · Rust 2024 edition değişiklikleri