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.
// 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.
// 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(())
}
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.
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.
impl Future<Output = T> döndüren bir tarif üretir.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; birFuturedöndürür.- Future lazy'dir —
.awaitveya poll edilene dek tek satır kod koşmaz. .awaitFuture'ı ilerletir; hazır değilse görevi askıya alıp kontrolü executor'a verir.- Bu, C++
std::async/ JSPromise'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.
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.
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;pollyaPoll::Ready(t)yaPoll::Pendingdöner.- Derleyici her
async fn'i, her.await'ün durak olduğu bir state machine enum'una çevirir. .awaitsonrası 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.
| Katman | Sağlayan | İçerik |
|---|---|---|
| Arabirim | std | Future, Poll, Context, Waker |
| Söz dizimi | derleyici | async/.await → state machine |
| Executor / runtime | tokio (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.
[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"] }
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.
// 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.
#[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ı)
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 vemainFuture'ınıblock_onile çalıştırır.tokio::spawnbir Future'ı scheduler'a verir, anındaJoinHandle<T>döner.JoinHandle'ı.awaitetmek görevin sonucunu beklemektir (asyncpthread_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.
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.
| Concurrency | Parallelism | |
|---|---|---|
| Tanım | çok görev "ilerleme hâlinde" | çok görev "aynı anda yürümede" |
| Tek çekirdek | mümkün (görev çoğullama) | imkânsız |
| Donanım | tek thread yeter | çok çekirdek şart |
| async ile | her zaman elde edersin | multi-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.
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,
.awaitile beklenir. - Her bağlantıyı
spawnedip 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.
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.
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
});
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
Sendolmalı. Rc,RefCellguard,std::sync::MutexGuard!Send'dir; await üzerinden taşımak derleme hatasıdır.- Çözüm:
Arc/tokio::sync::Mutexkullan ya da!Senddeğ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.
// 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.
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ştirici | Davranış | Tipik kullanım |
|---|---|---|
join! | hepsini bekle, tüm sonuçları topla | bağımsız işleri paralel toplama |
try_join! | hepsini bekle, ilk Err'de kısa devre | biri başarısızsa hepsini iptal |
select! | ilk biteni al, kalanı iptal et | yarış / iptal / zaman aşımı |
timeout | Future'a süre sınırı koy | askıda kalan I/O'yu kesme |
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ı
.awaitetmek süreleri toplar;join!onları eşzamanlı ilerletir (≈ en yavaş). try_join!ilkErr'de kısa devre yapar;select!ilk biteni alıp kalanı iptal eder.tokio::time::timeoutbir Future'a süre sınırı koyar, aşarsaErrdö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ü | Örnek | Doğru araç | Neden |
|---|---|---|---|
| I/O-bound | ağ, disk, DB sorgusu | async / tokio | bekleme thread'i bloklamaz, çok görev çoğullanır |
| CPU-bound | hash, sıkıştırma, render | thread / rayon | gerçek paralellik şart; askıya alınacak nokta yok |
| Karışık | I/O + ağır hesap | async + spawn_blocking | hesabı 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.
// 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
});
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_blockingbloklayıcı/ağır işi ayrı bir thread havuzuna taşır, runtime'ı serbest tutar.- Uyku için
tokio::time::sleepkullan;std::thread::sleepthread'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.
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:
// Üç 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}");
}
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şınaspawn. timeoutile 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.