Tüm Rust rehberleri
TEKNİK REHBER ASYNC TOKIO 2026

Async & Tokio
Future, await ve executor

async/await, Future trait, tokio runtime ve async vs thread; binlerce eşzamanlı I/O görevi — C epoll/callback ve thread-per-connection ile karşılaştırmalı.

00 Callback cehenneminden async/await'e

Thread-per-connection ölçeklenmez; el yazımı epoll state machine'leri okunmaz. Rust async, senkron görünümlü kodu eşzamanlı I/O'ya derler.

10.000 eşzamanlı bağlantı tutman gerekiyor. C/C++ dünyasında elindeki iki klasik seçeneği biliyorsun. Birincisi thread-per-connection: her bağlantıya bir pthread. Temiz görünür, bloklayıcı read()/write() yazarsın, ama her thread 1–8 MB stack ister; 10K thread = onlarca GB sanal bellek artı çekirdeğin scheduler'ında bağlam değişimi (context switch) fırtınası. İkincisi epoll + event loop: tek thread, on binlerce fd, ama bedeli kodu elinle bir state machine'e bölmen. Her bloklamayan çağrı EAGAIN dönebilir; yarım kalan işi bir struct'a saklar, epoll_wait dönünce kaldığın yerden devam edersin. Mantık callback'lere ve durum enum'larına dağılır.

echo_epoll.c
// C: epoll ile niyet basit — "oku, sonra yaz" — ama kod parçalanmış
struct conn { int fd; enum { READING, WRITING } st; char buf[4096]; size_t n; };

for (;;) {
    int r = epoll_wait(ep, evs, MAX, -1);
    for (int i = 0; i < r; i++) {
        struct conn *c = evs[i].data.ptr;
        if (c->st == READING) { /* read(); EAGAIN olabilir, durumu sakla */ }
        else               { /* write(); yarım yazıldıysa tekrar bekle */ }
        // "oku sonra yaz" basit niyeti 3 dala, 1 enum'a, 1 struct'a dağıldı
    }
}

Rust'ın async/await modeli üçüncü bir yolu sunar: kodu senkron görünümlü yazarsın — yukarıdan aşağı, dallanmasız — ama derleyici onu otomatik olarak bir state machine'e çevirir. Tek thread'de binlerce görev (task) yürür, hiçbir görev I/O beklerken thread'i bloklamaz. Yani epoll'ün ölçeklenmesini, thread kodunun okunabilirliğiyle birlikte alırsın.

main.rs
// Rust: aynı niyet, düz akış — derleyici state machine'i kendi üretir
async fn echo(mut sock: TcpStream) -> io::Result<()> {
    let mut buf = [0u8; 4096];
    let n = sock.read(&mut buf).await?;   // burada "bekler" ama thread'i bloklamaz
    sock.write_all(&buf[..n]).await?;   // yarım yazma yönetimi await'in içinde
    Ok(())
}
NOT

Buradaki .await, C'deki bloklayıcı read() gibi görünür ama davranışı EAGAIN + epoll_wait döngüsünün otomatikleştirilmiş halidir. Görev veri beklerken askıya alınır, thread başka göreve geçer — tıpkı senin elinle yazacağın event loop gibi, ama derleyici yazar.

thread-per-conn:  okunaklı kod  +  kötü ölçeklenme
epoll state machine: iyi ölçeklenme  +  okunmaz kod
async/await:      okunaklı kod  +  iyi ölçeklenme

Bu bölümde

  • Thread-per-connection okunaklıdır ama stack + context switch maliyetiyle ölçeklenmez.
  • epoll event loop ölçeklenir ama mantığı elle state machine'e bölmeyi gerektirir.
  • async/await senkron görünümlü kodu derleyici aracılığıyla bir state machine'e çevirir.
  • Sonuç: epoll'ün ölçeklenmesi + thread kodunun okunabilirliği.

01 async fn ve .await

Bir async fn gövdesini çalıştırmaz; bir Future döndürür. Future lazy'dir — await/poll edilene dek hiçbir şey olmaz.

İlk sürpriz: async fn fetch() çağırmak fonksiyonun gövdesini çalıştırmaz. Sana sadece bir Future nesnesi verir — "ileride bu işi yapacak tarif". Bu, C++ std::async ya da JavaScript Promise'inden kritik farktır: orada işi tetiklersin, geri planda koşmaya başlar. Rust'ta Future lazy'dir; onu birisi .await edip ilerletene dek tek bir CPU çevrimi harcanmaz.

main.rs
async fn fetch(url: &str) -> String {
    // ... ağ işi ...
    let body = format!("{url} cevabı");
    body
}

async fn run() {
    let fut = fetch("https://a.dev");  // HİÇBİR ŞEY çalışmadı — sadece Future kuruldu
    // burada fetch'in gövdesi henüz hiç yürütülmedi
    let body = fut.await;             // İŞTE ŞİMDİ gövde koşar, bitene dek ilerletilir
    println!("{body}");
}

.await iki şey yapar. Birincisi: Future'ı ilerletmeyi dener. Future hazırsa (örn. soket okunabilir) sonucu hemen verir. İkincisi: Future hazır değilse (veri yok), bulunduğun async fonksiyonu o noktadan askıya alır ve kontrolü çağırana — yani executor'a — geri verir. Executor başka görevleri yürütür; veri geldiğinde senin görevin kaldığı .await noktasından devam eder. Bu, C'de elinle yazdığın "EAGAIN gördüm, durumu sakla, epoll_wait'e dön" mantığının dil seviyesindeki karşılığıdır.

async fnGövdeyi çalıştırmaz; impl Future<Output = T> döndüren bir tarif üretir.
.awaitFuture'ı ilerletir; hazır değilse çağıran async fonksiyonu askıya alıp kontrolü executor'a verir.
lazyAwait/poll edilene dek Future içindeki kod hiç koşmaz — Promise/std::async'in tersine.
DİKKAT

Future'ı oluşturup .await etmeyi unutursan kodun hiç çalışmaz. Derleyici "unused Future that must be used" uyarısı verir. JavaScript'te "fire and forget" alışkanlığın varsa burada işe yaramaz: .await yoksa iş de yok.

Bu bölümde

  • async fn çağrısı gövdeyi çalıştırmaz; bir Future döndürür.
  • Future lazy'dir — .await veya poll edilene dek tek satır kod koşmaz.
  • .await Future'ı ilerletir; hazır değilse görevi askıya alıp kontrolü executor'a verir.
  • Bu, C++ std::async / JS Promise'inin eager (anında başlayan) modelinden temel bir farktır.

02 Future bir durum makinesidir

Future trait'i tek bir metoda dayanır: poll(). Derleyici her async fn'i, .await noktalarını duraklar yapan bir state machine'e çevirir.

Future, sihir değil — basit bir trait. Çekirdeği tek metot: poll. Executor bir görevi ilerletmek için poll çağırır; görev Poll::Ready(t) (bittim, sonuç bu) ya da Poll::Pending (henüz değil, beni sonra tekrar çağır) döner. Bu, epoll'ün "fd hazır mı?" sorusunun tip sistemindeki halidir.

future.rs
pub trait Future {
    type Output;
    // Pending dönerse: ilerleme yok; Waker tetiklenince tekrar poll edilecek
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

enum Poll<T> { Ready(T), Pending }

Asıl güzellik: sen poll yazmazsın. Derleyici, içinde n tane .await olan bir async fn'i, n+1 durumlu bir enum'a — gerçek bir state machine'e — derler. Her .await bir durak noktasıdır: makine o durumda durur, kontrolü bırakır, tekrar poll edildiğinde o durumdan devam eder. Yerel değişkenlerin (.await sonrası hâlâ kullanılanlar) bu enum'ın varyantlarında saklanır — yani C'de elinle struct conn'a koyduğun "kaldığım yer" alanlarını derleyici otomatik üretir.

async fn:  oku().await  →  işle()  →  yaz().await  →  bitti
state machine: [S0: oku bekliyor] → [S1: yaz bekliyor] → [S2: Ready]

Peki Pin<&mut Self> neden var? State machine, kendi içindeki bir veriye referans tutabilir (örn. okuma buffer'ına işaret eden bir slice). Böyle bir yapı self-referential'dir: bellekte taşınırsa (move) içindeki o referans bozulur — klasik dangling. Pin, "bu Future poll edilirken bellekte yer değiştiremez" garantisini tip seviyesinde verir. Çoğu zaman Pin'i hiç elle tutmazsın; tokio::pin! ya da heap'e koyan Box::pin halleder. Bilmen gereken tek şey: var olma sebebi self-referential state machine'lerin güvenliğidir.

NOT

Future'ın bedeli sıfıra yakındır: heap allocation yok (top-level görev hariç), her görev için ayrı stack yok. Bir async görev, durumunu tutan tek bir derleyici-üretimi struct'tan ibarettir. 10K görev = 10K küçük struct, 10K thread stack'i değil. Ölçeklenme farkının kaynağı budur.

Bu bölümde

  • Future = tek metotlu trait; poll ya Poll::Ready(t) ya Poll::Pending döner.
  • Derleyici her async fn'i, her .await'ün durak olduğu bir state machine enum'una çevirir.
  • .await sonrası yaşayan yerel değişkenler bu enum'ın varyantlarında saklanır.
  • Pin, self-referential Future'ların poll sırasında move edilmemesini garanti eder.

03 Runtime/executor gereksinimi

Future kendiliğinden ilerlemez; onu poll eden bir executor gerekir. std bir runtime içermez — Rust async, runtime'ı kasten dile gömmez.

Future tanımladın, ama onu kim poll edecek? poll'u tekrar tekrar çağıran, Pending dönenleri uyku noktalarına bağlayan (epoll/kqueue/IOCP üzerinden) ve hazır olunca yeniden poll eden bileşene executor (daha geniş paketiyle runtime) denir. Burada Go ve Node.js'ten kritik bir tasarım farkı var: std bir runtime içermez. Standart kütüphane sadece Future, Poll, Waker arabirimlerini tanımlar; gerçek event loop'u sana bırakır.

Neden? Çünkü Rust gömülü sistemden sunucuya kadar her yerde koşar. Tek bir zorunlu scheduler dayatmak, no_std bir mikrodenetleyiciyle yüksek-throughput bir ağ sunucusunu aynı kalıba sokmak olurdu. Rust bunun yerine arabirimi standartlaştırır, politikayı (iş çalma, thread sayısı, I/O backend) ekosisteme bırakır. Pratikte baskın seçim tokio'dur.

KatmanSağlayanİçerik
ArabirimstdFuture, Poll, Context, Waker
Söz dizimiderleyiciasync/.await → state machine
Executor / runtimetokio (vb.)scheduler, epoll/IOCP, timer, async I/O tipleri

tokio'yu projeye ekle. features = ["full"] öğrenirken pratiktir; üretimde sadece kullandığın özellikleri ("rt-multi-thread", "net", "io-util", "macros", "time") açarak derleme süresini ve ikili boyutunu kısarsın.

Cargo.toml
[dependencies]
# öğrenirken: tüm özellikler açık
tokio = { version = "1", features = ["full"] }

# üretimde: yalnızca gerekenler
# tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "macros"] }
NOT

Birden çok runtime vardır (async-std, smol, gömülü için embassy). Future trait ortak olduğundan tip seviyesinde uyumludurlar; ama I/O tipleri (örn. tokio'nun TcpStream'i) kendi runtime'ına bağlıdır. Pratik kural: tek bir runtime seç ve ekosisteminde kal. tokio en geniş kütüphane desteğine sahiptir.

Bu bölümde

  • Future'ı executor poll eder; runtime = scheduler + I/O backend + timer'lar.
  • std bilinçli olarak runtime içermez — sadece Future/Poll/Waker arabirimlerini tanımlar.
  • Politika ekosisteme bırakıldı: gömülüden sunucuya tek scheduler dayatılmaz.
  • Baskın seçim tokio; features = ["full"] öğrenmek için, daraltılmış set üretim için.

04 #[tokio::main] ve task spawn

#[tokio::main] runtime'ı kurar; tokio::spawn bir görevi scheduler'a verir. Task = yeşil iş parçacığı, OS thread'i değil.

main async olamaz — programın giriş noktası bir Future'ı bekleyecek executor'a ihtiyaç duyar. #[tokio::main] makrosu bu plaka kodu yazar: bir runtime oluşturur, main'in döndürdüğü Future'ı block_on ile çalıştırır.

main.rs
// Makro bunu üretir: Runtime::new().block_on(async { ... })
#[tokio::main]
async fn main() {
    println!("runtime hazır, main bir Future olarak koşuyor");
}

Eşzamanlılık için görev (task) açarsın. tokio::spawn bir Future'ı scheduler'a teslim eder ve hemen bir JoinHandle<T> döner — görev arka planda bağımsız ilerler (burada Future'ın lazy kuralı bozulmaz: spawn, ilerletme görevini executor'a devrettiği için iş başlar). JoinHandle, C'deki pthread_join'in async eşdeğeridir: .await edersen görevin sonucunu beklersin.

main.rs
#[tokio::main]
async fn main() {
    let handle: JoinHandle<u64> = tokio::spawn(async {
        // bağımsız bir görev — kendi state machine'i
        (1..=100).sum()
    });

    // main bu sırada başka iş yapabilir...
    let toplam = handle.await.unwrap();  // JoinHandle .await = görevin sonucu
    println!("toplam = {toplam}");
}

Kritik zihinsel model: task bir OS thread'i değildir. Çoğu zaman "yeşil iş parçacığı" (green thread) denir — runtime tarafından zamanlanan, kullanıcı uzayında yaşayan hafif bir yürütme birimi. Varsayılan multi-thread scheduler bir thread havuzu (genelde çekirdek sayısı kadar) tutar ve binlerce task'ı bu birkaç thread üzerinde iş çalma (work-stealing) ile çoğullar (multiplex). Bir task .await'te askıya alındığında, çalıştığı thread başka bir hazır task'a geçer. 10K task açmak 10K thread değil, 10K küçük state machine demektir.

10.000 task  →  work-stealing scheduler  →  N OS thread (N ≈ çekirdek sayısı)
DİKKAT

Multi-thread runtime'da bir task herhangi bir thread'de — ve .await sonrası başka bir thread'de — koşabilir. Bu yüzden task'lar arası paylaşılan durum thread-safe olmalı (bkz. s6: Send). Bu, tek thread'li Node.js event loop'undan önemli bir ayrımdır.

Bu bölümde

  • #[tokio::main] runtime'ı kurar ve main Future'ını block_on ile çalıştırır.
  • tokio::spawn bir Future'ı scheduler'a verir, anında JoinHandle<T> döner.
  • JoinHandle.await etmek görevin sonucunu beklemektir (async pthread_join).
  • Task = yeşil iş parçacığı; binlerce task work-stealing ile birkaç OS thread'e çoğullanır.

05 Async I/O

tokio'nun TcpListener/TcpStream'i async accept/read/write sunar. Tek thread'de binlerce görev = concurrency; çok çekirdekte aynı anda yürütme = parallelism.

tokio, std'nin bloklayıcı ağ tiplerinin async ikizlerini sağlar. İmzalar tanıdık ama metotlar Future döndürür; .await ile beklersin. Aşağıda klasik accept döngüsü: her bağlantıyı ayrı bir task'a verir, döngü hemen yeni accept'e döner — yani sunucu bir bağlantıyı işlerken diğerlerini kabul etmeyi bekletmez.

main.rs
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8080").await?;
    loop {
        let (mut sock, _) = listener.accept().await?;  // async accept
        tokio::spawn(async move {                       // her bağlantı kendi task'ı
            let mut buf = [0u8; 1024];
            while let Ok(n) = sock.read(&mut buf).await {
                if n == 0 { break; }              // karşı taraf kapattı
                if sock.write_all(&buf[..n]).await.is_err() { break; }
            }
        });
    }
}

Burada concurrency ile parallelism ayrımı netleşir; C/C++'ta sık karıştırılır. Concurrency: birden çok görevin ilerleme hâlinde olması — biri I/O beklerken diğerine geçilmesi. Bu, tek bir thread'de bile mümkündür; epoll'ün yaptığı tam olarak budur. Parallelism: birden çok görevin aynı anda farklı CPU çekirdeklerinde yürümesi; donanım paralelliği gerektirir.

ConcurrencyParallelism
Tanımçok görev "ilerleme hâlinde"çok görev "aynı anda yürümede"
Tek çekirdekmümkün (görev çoğullama)imkânsız
Donanımtek thread yeterçok çekirdek şart
async ileher zaman elde edersinmulti-thread runtime ile

tokio'nun varsayılan multi-thread runtime'ı ikisini birleştirir: tek thread'de bile binlerce bağlantıyı I/O üzerinde çoğullar (concurrency) ve birden çok çekirdekte task'ları gerçekten paralel koşturur (parallelism). 10K bağlantı için 10K thread açmak yerine, birkaç thread üzerinde 10K task çoğullanır.

NOT

read/write metotları AsyncReadExt/AsyncWriteExt trait'lerinden gelir — kullanmak için onları use etmen şart, yoksa "no method named read" hatası alırsın. Bu, Rust'ın "trait metotları görünür olmalı" kuralının klasik bir tezahürüdür.

Bu bölümde

  • tokio, std ağ tiplerinin async ikizlerini sunar; metotlar Future döndürür, .await ile beklenir.
  • Her bağlantıyı spawn edip accept döngüsünü bekletmemek temel desendir.
  • Concurrency = çok görev ilerleme hâlinde (tek thread'de bile); parallelism = aynı anda çok çekirdekte.
  • Multi-thread runtime ikisini birleştirir: I/O çoğullama + çekirdekler arası gerçek paralellik.

06 Send across await ve tuzaklar

Bir .await'i geçen her değişken state machine'de saklanır; multi-thread'de bu değerlerin Send olması gerekir. Rc/RefCell guard'ı await üzerinden taşımak derleme hatasıdır.

s2'den hatırla: .await noktasından sonra hâlâ kullanılan yerel değişkenler, derleyicinin ürettiği state machine struct'ında yaşar. Multi-thread runtime'da bir task, bir .await'te askıya alınıp başka bir thread'de devam edebilir. Yani state machine'in tamamı thread'ler arasında taşınabilmeli — bu da içindeki tüm "await'i geçen" verilerin Send olmasını gerektirir.

Klasik tuzak: thread-safe olmayan bir tipi (Rc, ya da RefCell'in Ref/RefMut guard'ı) bir .await'in iki yanında canlı tutmak. Bu tipler kasıtla !Send'dir; derleyici task'ı başka thread'e taşıyamaz ve tokio::spawn reddeder.

main.rs
use std::rc::Rc;

// HATA: Rc !Send; await üzerinden canlı kaldığı için task Send değil
tokio::spawn(async {
    let data = Rc::new(42);
    yield_now().await;          // burada askıya alınır; `data` hâlâ canlı
    println!("{}", data);       // await'in diğer yanında kullanılıyor
});
// error: future cannot be sent between threads safely  →  `Rc<i32>` is not `Send`

İki çözüm var. Birincisi: thread-safe karşılığı kullan — Rc yerine Arc, RefCell yerine tokio::sync::Mutex (kilidi await üzerinden tutmak gerekiyorsa) ya da std::sync::Mutex (kilit kısa ve await'siz tutuluyorsa). İkincisi: !Send değeri .await'ten önce bir kapsamda kapat (drop et), böylece state machine'e hiç sızmaz.

main.rs
use std::sync::Arc;

// Çözüm 1: Send olan Arc
tokio::spawn(async {
    let data = Arc::new(42);    // Arc Send'dir
    yield_now().await;
    println!("{}", data);        // sorun yok
});

// Çözüm 2: !Send değeri await'ten önce drop et (kapsam içinde tut)
tokio::spawn(async {
    {
        let tmp = Rc::new(1);     // Rc burada kullanılır...
        do_sync(&tmp);
    }                            // ...ve burada drop edilir — await'i geçmez
    yield_now().await;            // artık state machine'de !Send bir şey yok
});
DİKKAT

std::sync::MutexGuard !Send'dir; kilidi .await üzerinden tutarsan task Send olmaktan çıkar. Kuralı şöyle düşün: kilidi await'ten önce bırak, ya da async-aware tokio::sync::Mutex kullan (onun guard'ı await üzerinden taşınabilir). Bu hata, C'de mutex tutarken bloklayıcı çağrı yapıp deadlock'a benzer ama burada derleme zamanında yakalanır.

Bu bölümde

  • .await'i geçen yerel değişkenler state machine'de saklanır.
  • Multi-thread runtime task'ı thread'ler arası taşıyabilir → await'i geçen değerler Send olmalı.
  • Rc, RefCell guard, std::sync::MutexGuard !Send'dir; await üzerinden taşımak derleme hatasıdır.
  • Çözüm: Arc/tokio::sync::Mutex kullan ya da !Send değeri await'ten önce drop et.

07 Eşzamanlı görev birleştirme

join! hepsini bekler, select! ilk biteni alır, timeout süre koyar. Sıralı .await ile eşzamanlı çalıştırma arasındaki performans farkı kritiktir.

En sık yapılan performans hatası: bağımsız işleri sıralı .await etmek. Üç HTTP isteğini art arda await edersen toplam süre = sürelerin toplamı; oysa hepsi bağımsızsa aynı anda ilerlemeleri ve toplam sürenin en yavaşı kadar olması gerekir. tokio::join! tam bunu yapar: birden çok Future'ı aynı görevde eşzamanlı ilerletir, hepsi bitince sonuçları tuple olarak verir.

main.rs
// YANLIŞ: sıralı — toplam ≈ a + b + c
let ra = fetch("a").await;
let rb = fetch("b").await;
let rc = fetch("c").await;

// DOĞRU: eşzamanlı — toplam ≈ max(a, b, c)
let (ra, rb, rc) = tokio::join!(fetch("a"), fetch("b"), fetch("c"));

// Herhangi biri hata dönebiliyorsa: ilk Err'de kısa devre yapar
let (ra, rb) = tokio::try_join!(get("a"), get("b"))?;

select! farklı bir desen: birden çok Future'ı eşzamanlı bekler ama ilk biten kazanır; geri kalanlar iptal edilir (drop edilir). C'de select()/poll()'un "hangi fd önce hazır" sorusunun async hâlidir. Tipik kullanım: bir işi bir iptal sinyaline ya da zaman aşımına karşı yarıştırmak.

main.rs
use tokio::time::{sleep, timeout, Duration};

// select!: ilk biteni al, diğerini iptal et
tokio::select! {
    res = fetch("hizli")   => println!("önce fetch bitti: {res}"),
    _   = sleep(Duration::from_secs(2)) => println!("2 sn doldu, vazgeç"),
}

// timeout: Future'ı süreyle sınırla — aşarsa Err(Elapsed)
match timeout(Duration::from_secs(3), fetch("a")).await {
    Ok(body) => println!("geldi: {body}"),
    Err(_)   => println!("zaman aşımı"),
}
BirleştiriciDavranışTipik kullanım
join!hepsini bekle, tüm sonuçları toplabağımsız işleri paralel toplama
try_join!hepsini bekle, ilk Err'de kısa devrebiri başarısızsa hepsini iptal
select!ilk biteni al, kalanı iptal etyarış / iptal / zaman aşımı
timeoutFuture'a süre sınırı koyaskıda kalan I/O'yu kesme
DİKKAT

select! kazanmayan kolu iptal eder — yani o Future bir .await noktasında yarıda kesilir (cancellation). Yarıda kesilen iş yan etki bırakmışsa (yarım yazılan dosya, tutulan kilit) tutarsızlık doğabilir. Kolların cancel-safe olduğundan emin ol; tokio dokümanı her metot için bunu belirtir.

Bu bölümde

  • Bağımsız işleri sıralı .await etmek süreleri toplar; join! onları eşzamanlı ilerletir (≈ en yavaş).
  • try_join! ilk Err'de kısa devre yapar; select! ilk biteni alıp kalanı iptal eder.
  • tokio::time::timeout bir Future'a süre sınırı koyar, aşarsa Err döner.
  • select! iptali (cancellation) yan etkili işlerde tutarsızlık yaratabilir — cancel-safe ol.

08 Async vs thread

I/O-bound iş → async; CPU-bound iş → thread/rayon. Bloklayıcı bir çağrı runtime thread'ini kilitler; çözüm spawn_blocking.

async her şeyin çözümü değildir. Doğru ayrım iş yükünün doğasındadır. I/O-bound iş (ağ, disk, bekleme) çoğunlukla beklemekle geçer; CPU boştadır. Burada async ideal: bekleyen görev thread'i serbest bırakır, binlercesi birkaç thread'e sığar. CPU-bound iş (şifreleme, sıkıştırma, sayısal hesap) çekirdeği doyurur; askıya alacak bir "bekleme" noktası yoktur. Burada async hiçbir şey kazandırmaz — gerçek paralellik için OS thread'leri ya da rayon gibi bir veri-paralel kütüphane gerekir.

İş türüÖrnekDoğru araçNeden
I/O-boundağ, disk, DB sorgusuasync / tokiobekleme thread'i bloklamaz, çok görev çoğullanır
CPU-boundhash, sıkıştırma, renderthread / rayongerçek paralellik şart; askıya alınacak nokta yok
KarışıkI/O + ağır hesapasync + spawn_blockinghesabı ayrı havuza taşı, runtime'ı boş tut

En tehlikeli tuzak: async bir görevin içinde bloklayıcı bir çağrı yapmak — uzun süren senkron hesap, std::thread::sleep, ya da bloklayan bir C kütüphane çağrısı. async görev yürüten bir runtime thread'i, .await'e gelene dek kooperatif çalışır; bloklayıcı çağrı o thread'i ele geçirir, üzerindeki tüm diğer task'lar aç kalır. Bu, tek thread'li event loop'u kilitlemenin tokio karşılığıdır.

main.rs
// YANLIŞ: bloklayıcı hesap runtime thread'ini ele geçirir
tokio::spawn(async {
    let h = expensive_hash(&data);  // 500 ms CPU — bu thread'deki diğer task'lar donar
    use_it(h);
});

// DOĞRU: bloklayıcı işi ayrı thread havuzuna taşı
tokio::spawn(async {
    let h = tokio::task::spawn_blocking(move || {
        expensive_hash(&data)        // ayrı blocking-pool thread'inde koşar
    }).await.unwrap();
    use_it(h);                       // runtime thread'i boş kaldı, diğer task'lar akmaya devam etti
});
NOT

spawn_blocking tokio'nun ayrı bir blocking thread pool'unda iş koşturur ve sonucu .await edilebilir bir JoinHandle ile geri verir. Ağır CPU işi için rayon ile birleştirebilir, async dünyaya köprüyü spawn_blocking (veya bir oneshot kanal) ile kurabilirsin. Uyku için her zaman tokio::time::sleep kullan — std::thread::sleep değil.

Bu bölümde

  • I/O-bound iş async'e, CPU-bound iş thread/rayon'a aittir; doğru ayrım iş yükünün doğasıdır.
  • async görev içinde bloklayıcı çağrı, runtime thread'ini ele geçirip diğer tüm task'ları aç bırakır.
  • spawn_blocking bloklayıcı/ağır işi ayrı bir thread havuzuna taşır, runtime'ı serbest tutar.
  • Uyku için tokio::time::sleep kullan; std::thread::sleep thread'i bloklar.

09 Gerçek örnek

Öğrendiğimiz her şeyi tek dosyada birleştiren, çalışan bir async TCP echo sunucusu — her bağlantı kendi task'ında, eşzamanlı binlerce istemciye dayanıklı.

Aşağıdaki sunucu bu rehberin tamamını özetler: #[tokio::main] ile runtime, async accept döngüsü, bağlantı başına spawn, AsyncReadExt/AsyncWriteExt ile async I/O ve timeout ile boşta kalan bağlantıyı kesme. Tek thread'li bir C epoll sunucusunun yaptığı işi, düz okunan kodla yapar — ama varsayılan multi-thread runtime sayesinde çekirdekler arasında da ölçeklenir.

main.rs
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{timeout, Duration};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("echo sunucu :8080 dinlemede");

    loop {
        let (sock, peer) = listener.accept().await?;   // async accept — bloklamaz
        // her bağlantı bağımsız bir task; accept döngüsü beklemez
        tokio::spawn(async move {
            if let Err(e) = handle(sock).await {
                eprintln!("{peer} hata: {e}");
            }
        });
    }
}

async fn handle(mut sock: tokio::net::TcpStream) -> std::io::Result<()> {
    let mut buf = [0u8; 1024];
    loop {
        // 30 sn veri gelmezse bağlantıyı kes (askıda kalan istemciyi temizle)
        let n = match timeout(Duration::from_secs(30), sock.read(&mut buf)).await {
            Ok(Ok(0))  => return Ok(()),   // 0 bayt = karşı taraf kapattı
            Ok(Ok(n))  => n,
            Ok(Err(e)) => return Err(e),     // soket hatası
            Err(_)      => return Ok(()),   // timeout: sessizce kapat
        };
        sock.write_all(&buf[..n]).await?;       // geleni aynen geri yaz
    }
}

Test etmek için ayrı bir terminalde nc 127.0.0.1 8080 ile bağlan, bir şey yaz — aynen geri gelir. Aynı anda onlarca nc oturumu açabilirsin; her biri kendi task'ında yürür, hiçbiri diğerini bekletmez. Bunu bir C epoll sunucusuyla kıyasla: orada bu mantık (accept, durum izleme, kısmi okuma/yazma, zaman aşımı) onlarca satır el yazımı state machine olurdu; burada düz, sıralı okunan async kod.

İstemci tarafında da eşzamanlılığın gücünü join! ile görelim — bağımsız işleri tek thread'de, tek görevde paralel yürüten son bir örnek:

main.rs
// Üç bağımsız işi eşzamanlı yürüt — toplam ≈ en yavaşı, toplamı değil
#[tokio::main]
async fn main() {
    let (a, b, c) = tokio::join!(
        fetch("https://x.dev/1"),
        fetch("https://x.dev/2"),
        fetch("https://x.dev/3"),
    );
    println!("{a} {b} {c}");
}
NOT

Buradan ileri yol: kanal tabanlı görevler arası iletişim için tokio::sync::mpsc; paylaşılan durum için Arc<Mutex<T>>; HTTP için reqwest (istemci) ve axum/hyper (sunucu); akışlar için Stream trait'i. Hepsi aynı Future/tokio temeline oturur — bu rehberdeki zihinsel model değişmeden ölçeklenir.

Bu bölümde

  • Tam echo sunucusu: #[tokio::main] + async accept döngüsü + bağlantı başına spawn.
  • timeout ile askıda kalan bağlantıyı kesmek üretim kalitesinde dayanıklılık sağlar.
  • C epoll state machine'inin onlarca satırı, düz okunan birkaç satır async koda iner.
  • İstemci tarafında join! bağımsız işleri eşzamanlı yürütür; ileri adım kanallar, reqwest, axum.