Ders 07 / 10
`async`/`await`
Duraklama noktaları kaynakta görünür yazılır: bir kez duraklayan bir gövde iki yoklamada, hemen hazır olan bir gövde bir yoklamada tamamlanır; içteki bir geleceği bekleyen dış bir gövde ise başkası bitince ilerler.
Yayın Güncelleme
Bu derste
- Bir "await" ifadesinin bir geleceği yoklamaktan başka bir şey yapmadığını ve dönüş noktasını kaynakta işaretlediğini gösterir
- Bir kez duraklayan ve hemen hazır olan iki gövdenin yoklama sayılarını karşılaştırarak duraklama noktasının koşuma etkisini ölçer
- İçteki bir geleceği bekleyen dış bir gövdenin başkası bitince sınıfına girdiğini, Go'nun Eşzamanlılık kursunun ölçtüğü bitiş sorusundan farkını göstererek açıklar
İçindekiler
Önceki ders HemenHazir tipini kurdu: poll çağrıldığı anda biten, hiçbir zaman Pending
dönmeyen bir gelecek. Bu, mümkün en yalın gövdeydi — durmadan geçen bir gövde. Bu ders aynı
sözleşmeyi bir kez duran bir gövdeyle genişletiyor ve Rust’ın bu gövdeleri elle yazmak yerine
üretmesini sağlayan yazımı tanıtıyor: async ve await. Sorulan soru aynı kalıyor — bu iş
ilerlemek için neye ihtiyaç duyuyor — ama şimdi yanıt kaynakta görünür bir işaretle veriliyor.
Önceki dersteki HemenHazir ve bir sonraki bölümdeki BirKezDuraklar, Future trait’ini
elle gerçekleştiren tiplerdi: poll yöntemini kendi elleriyle yazdılar. Gerçek programlarda
bir eşzamansız gövdenin çoğu satırı elle yazılmıyor — async anahtar sözcüğüyle işaretlenmiş
sıradan bir fonksiyon gövdesi olarak yazılıyor ve derleyici, bu gövdeyi otomatik olarak bir
Future gerçekleştirimine çeviriyor. async ve await, elle yazılan poll mantığının
kaynak düzeyindeki kısaltması; bu ders o kısaltmanın altında hâlâ aynı sözleşmenin —
Ready/Pending ikilisinin — çalıştığını gösteriyor.
Duraklama Noktası Kaynakta Yazılıdır
Önceki dersteki HemenHazir, Future trait’ini elle gerçekleştiriyordu. BirKezDuraklar
aynı trait’i, bir kez Pending dönecek biçimde gerçekleştiriyor: ilk çağrıda durumunu
değiştirip kendini bir sonraki poll için uyandırıyor, ikinci çağrıda tamamlanıyor.
// ilerle.rs — cekirdegin kesiti: yurutucu, bir kez duraklayan gelecek, hemen hazir gelecek,
// ve icteki gelecegi bekleyen bir eszamansiz fonksiyon.
use std::future::Future;
use std::pin::Pin;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, Wake, Waker};
pub struct Uyandirici {
pub hazir: Mutex<bool>,
pub kosul: Condvar,
}
impl Wake for Uyandirici {
fn wake(self: Arc<Self>) {
*self.hazir.lock().unwrap() = true;
self.kosul.notify_one();
}
}
pub fn kostur<F: Future>(gorev: F) -> (F::Output, u32) {
let mut gorev = Box::pin(gorev);
let u = Arc::new(Uyandirici {
hazir: Mutex::new(false),
kosul: Condvar::new(),
});
let waker: Waker = u.clone().into();
let mut baglam = Context::from_waker(&waker);
let mut yoklama = 0;
loop {
yoklama += 1;
match gorev.as_mut().poll(&mut baglam) {
Poll::Ready(x) => return (x, yoklama),
Poll::Pending => {
let mut h = u.hazir.lock().unwrap();
while !*h {
h = u.kosul.wait(h).unwrap();
}
*h = false;
}
}
}
}
pub struct BirKezDuraklar {
pub duraklandi: bool,
pub ad: &'static str,
pub kanal: Sender<String>,
}
impl Future for BirKezDuraklar {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<&'static str> {
if self.duraklandi {
self.kanal.send(self.ad.to_string()).unwrap();
Poll::Ready(self.ad)
} else {
self.duraklandi = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
pub struct HemenHazir {
pub ad: &'static str,
pub kanal: Sender<String>,
}
impl Future for HemenHazir {
type Output = &'static str;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<&'static str> {
self.kanal.send(self.ad.to_string()).unwrap();
Poll::Ready(self.ad)
}
}
pub async fn disariyi_bekle(ic: BirKezDuraklar, kanal: Sender<String>) -> &'static str {
let _ = ic.await;
kanal.send(String::from("gorev-await-eden")).unwrap();
"await-eden"
}
disariyi_bekle bir async fn. Bu yazım, gövdesini elle bir Future gerçekleştirimine
çevirmekten kurtarıyor: derleyici, gövdeyi kendi kendine bir durum makinesine dönüştürüyor ve
bu makine Future trait’ini kendisi gerçekleştiriyor. Gövde içindeki ic.await satırı, bu
dönüşümün gözle görülür tek izi — duraklama noktası. Derleyici bu satırı gördüğünde,
durum makinesine “burada duraklayabilirim” diyen bir dal ekliyor: icnin pollu Pending
dönerse, dış gövde de kendi pollunu Pending ile bitiriyor ve bir sonraki çağrıda kaldığı
yerden devam ediyor. .await bir makro çağrısı değil, bir işleç; kod bloğu dışında da adı
serbestçe geçebilir.
Üretilen durum makinesi kavram olarak, Rust Temelleri kursunda kurulan numaralandırmaya
yakın: sonlu sayıda ayrı durumdan biri her seferinde etkin oluyor, hangi durumun etkin olduğu
bir önceki poll çağrısının nerede kaldığına bağlı. Fark, bu numaralandırmanın elle
yazılmamış olması — derleyici, gövdedeki her .await sayısı kadar duraklama durumu üretiyor
ve aralarındaki geçişi kendisi kuruyor. disariyi_beklede tek bir .await var; bu yüzden
derleyicinin ürettiği durum makinesi yalnız iki durum arasında geçiş yapıyor: icyi
bekleyen durum ve tamamlanmış durum.
Fonksiyonun imzası — async fn disariyi_bekle(...) -> &'static str — kaynakta yazılan dönüş
tipi &'static str gibi görünse de, gerçek dönüş değeri derleyicinin ürettiği bu durum
makinesi: Future<Output = &'static str> sözleşmesini taşıyan, adı kaynakta hiç geçmeyen bir
tip. kostur<F: Future> imzasındaki F::Output ilişkili tipi tam olarak bunu karşılıyor —
kostur, kendisine hangi somut durum makinesinin verildiğini bilmeden, yalnız Future
sözleşmesine güvenerek çalışıyor. disariyi_bekle(ic, gonder.clone()) çağrısının kendisi de
hiçbir şeyi çalıştırmıyor; önceki derste kurulan tembellik kuralı burada da geçerli — çağrı
yalnız durum makinesinin ilk durumunu kuruyor, kostura verilene kadar hiçbir alanı
işlemiyor.
Duraklama Sayısı Koşuma Yansıyor
Aşağıdaki program üç gövdeyi aynı yürütücüyle koşturuyor: bir kez duraklayan BirKezDuraklar,
hiç duraklamayan HemenHazir ve içinde bir BirKezDuraklari bekleyen disariyi_bekle.
// yoklama-sayilari.rs — bir kez duraklayan, hemen hazir olan ve icini bekleyen govdelerin
// yoklama sayilari.
mod ilerle;
use ilerle::{disariyi_bekle, kostur, BirKezDuraklar, HemenHazir};
use std::sync::mpsc::channel;
fn main() {
let (gonder, al) = channel::<String>();
let (deger_a, yoklama_a) = kostur(BirKezDuraklar {
duraklandi: false,
ad: "gorev-yoklanan-a",
kanal: gonder.clone(),
});
let (deger_b, yoklama_b) = kostur(HemenHazir {
ad: "gorev-hazir-b",
kanal: gonder.clone(),
});
let ic = BirKezDuraklar {
duraklandi: false,
ad: "gorev-ic",
kanal: gonder.clone(),
};
let (deger_c, yoklama_c) = kostur(disariyi_bekle(ic, gonder.clone()));
drop(gonder);
let mut kayitlar: Vec<String> = al.try_iter().collect();
kayitlar.sort();
println!("degerler: a={} b={} c={}", deger_a, deger_b, deger_c);
println!("yoklama sayilari: a={} b={} c={}", yoklama_a, yoklama_b, yoklama_c);
println!();
println!("kayitlar ({}):", kayitlar.len());
for k in &kayitlar {
println!(" {}", k);
}
}
degerler: a=gorev-yoklanan-a b=gorev-hazir-b c=await-eden yoklama sayilari: a=2 b=1 c=2 kayitlar (4): gorev-await-eden gorev-hazir-b gorev-ic gorev-yoklanan-a
AS2. gorev-yoklanan-a iki yoklamada bitti: ilk çağrı duraklandiyi true yapıp
Pending döndü, ikinci çağrı kanal.send çağırıp Ready döndü. gorev-hazir-b tek
yoklamada bitti çünkü hiç duraklamadı. Bu iki sayı, aynı yürütücüyle koşturulan iki farklı
gövdenin kaç kez yoklandığının kaynaktaki davranışlarından doğrudan çıkarılabilir olduğunu
gösteriyor — yoklama sayısı burada ortama bağlı bir yan bilgi değil, gövdenin kendi
mantığından gelen belirlenimci bir sonuç. Beş ayrı koşumda üç sayı da birebir aynı kaldı.
disariyi_beklenin yoklama sayısı da ikiydi, ama nedeni farklı. Bu gövde ic.awaite
ulaştığında, kendi pollu içeriden icnin pollunu çağırıyor. İlk dış çağrıda ic
Pending döndürüyor, dış gövde de bunu olduğu gibi yukarı taşıyıp Pending dönüyor. İkinci
dış çağrıda ic tamamlanıyor (gorev-ic kaydı burada yazılıyor), .await satırı bitiyor,
kalan gövde (gorev-await-eden kaydı) aynı çağrı içinde çalışıp Ready dönüyor. İki yoklama
da, ama biri kendi durumundan, öbürü içindeki başka bir geleceğin durumundan geliyor.
Duraklama Noktası Sayısı Yoklama Sayısını Belirler
Bir gövde tek bir .await ile sınırlı değil; art arda birden fazla duraklama noktası
taşıyabilir. Aşağıdaki kesit disariyi_beklenin yerine, iki ayrı BirKezDurakları sırayla
bekleyen bir gövde tanımlıyor. HemenHazir ve disariyi_bekle bu ölçümde gerekmediği için
kesit dışında bırakılıyor.
// ilerle.rs — cekirdegin kesiti degisti: iki ardisik duraklama noktasi tasiyan bir govde
// ekleniyor, HemenHazir ve disariyi_bekle bu olcumde gerekmiyor.
use std::future::Future;
use std::pin::Pin;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Condvar, Mutex};
use std::task::{Context, Poll, Wake, Waker};
pub struct Uyandirici {
pub hazir: Mutex<bool>,
pub kosul: Condvar,
}
impl Wake for Uyandirici {
fn wake(self: Arc<Self>) {
*self.hazir.lock().unwrap() = true;
self.kosul.notify_one();
}
}
pub fn kostur<F: Future>(gorev: F) -> (F::Output, u32) {
let mut gorev = Box::pin(gorev);
let u = Arc::new(Uyandirici {
hazir: Mutex::new(false),
kosul: Condvar::new(),
});
let waker: Waker = u.clone().into();
let mut baglam = Context::from_waker(&waker);
let mut yoklama = 0;
loop {
yoklama += 1;
match gorev.as_mut().poll(&mut baglam) {
Poll::Ready(x) => return (x, yoklama),
Poll::Pending => {
let mut h = u.hazir.lock().unwrap();
while !*h {
h = u.kosul.wait(h).unwrap();
}
*h = false;
}
}
}
}
pub struct BirKezDuraklar {
pub duraklandi: bool,
pub ad: &'static str,
pub kanal: Sender<String>,
}
impl Future for BirKezDuraklar {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<&'static str> {
if self.duraklandi {
self.kanal.send(self.ad.to_string()).unwrap();
Poll::Ready(self.ad)
} else {
self.duraklandi = true;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
pub async fn iki_kez_bekle(ic1: BirKezDuraklar, ic2: BirKezDuraklar) -> &'static str {
let _ = ic1.await;
let _ = ic2.await;
"iki-kez-bekleyen"
}
// iki-duraklama.rs — iki ardisik duraklama noktasi tasiyan bir govdenin yoklama sayisi.
mod ilerle;
use ilerle::{iki_kez_bekle, kostur, BirKezDuraklar};
use std::sync::mpsc::channel;
fn main() {
let (gonder, al) = channel::<String>();
let ic1 = BirKezDuraklar {
duraklandi: false,
ad: "gorev-ic-birinci",
kanal: gonder.clone(),
};
let ic2 = BirKezDuraklar {
duraklandi: false,
ad: "gorev-ic-ikinci",
kanal: gonder.clone(),
};
let (deger, yoklama) = kostur(iki_kez_bekle(ic1, ic2));
drop(gonder);
let mut kayitlar: Vec<String> = al.try_iter().collect();
kayitlar.sort();
println!("deger: {}", deger);
println!("yoklama sayisi: {}", yoklama);
println!("kayitlar ({}):", kayitlar.len());
for k in &kayitlar {
println!(" {}", k);
}
}
deger: iki-kez-bekleyen yoklama sayisi: 3 kayitlar (2): gorev-ic-birinci gorev-ic-ikinci
AS3. İki duraklama noktası üç yoklamaya karşılık geliyor: ilk çağrı ic1i Pending
yapıyor, ikinci çağrı ic1i bitirip ic2nin ilk pollunu tetikliyor (o da Pending
dönüyor, aynı dış çağrı içinde), üçüncü çağrı ic2yi bitirip gövdenin kalanını çalıştırıyor.
Genel kural buradan çıkıyor: art arda gelen duraklama noktası, art arda gelen
yoklama gerektiriyor — son çağrı her zaman ilerlemeyi tamamlayan, Pending döndürmeyen
çağrı. Bu sayı da beş ayrı koşumda birebir aynı kaldı; kaynaktaki .await sayısı, koşumdaki
yoklama sayısını önceden belirliyor. gorev-ic-ikinci kaydı hiçbir koşumda
gorev-ic-birinci kaydı oluşmadan önce yazılamaz — ikinci duraklama noktası, ic2nin ilk
pollu ancak ic1 tamamlandıktan sonra çağrıldığı için, birincisi bitmeden hiç
değerlendirilmiyor. Bu sıra, defterde alfabetik olarak sıralandığı için değil, durum
makinesinin dallarını yalnız bir öncekini tamamladıktan sonra geçebildiği için garanti
altında.
Beklemek ile Yoklanmak Aynı Şey Değil
gorev-ic — disariyi_beklenin içindeki BirKezDuraklar örneği — yoklanınca sınıfına
giriyor: gorev-yoklanan-a ile aynı tip, aynı davranış, .await içinde kullanılmış olması
sınıfını değiştirmiyor. Buna karşılık gorev-await-eden kaydı — disariyi_beklenin kendi
tamamlanma satırı — başkası bitince sınıfına giriyor. Fark şu: gorev-ic doğrudan
yoklanıyor ve kendi ilerlemesi için yalnız tekrar tekrar çağrılmaya ihtiyaç duyuyor;
disariyi_bekle ise ic tamamlanmadan hiçbir zaman kendi son satırına ulaşamıyor —
ilerlemesi kendi yoklanmasına değil, başka bir görevin bitmesine bağlı.
Go’nun Eşzamanlılık kursu bir birimin nasıl duracağını ölçmüştü — kendi, beklediği geldiği
için, iptal edildiği için ya da hiç durmadan. Bu ders aynı soruyu sormuyor; burada ölçülen
bitiş değil, duraklayıp nereden devam ettiği. disariyi_bekle hiçbir zaman durmuyor,
yalnız bir kez Pending dönüp aynı noktadan devam ediyor — ve bu devam noktası, .await
yazıldığı satırla birebir eşleşiyor. Kaynaktaki tek bir satır, koşumdaki duraklama noktasını
önceden gösteriyor.
Bu ayrım, İş Parçacıkları konusundaki kanal bekleyişiyle karşılaştırıldığında daha net
görünüyor. O konuda bir iş parçacığı bir kanaldan haber beklerken işletim sistemi tarafından
askıya alınıyordu; iş parçacığının kendisi, beklediği süre boyunca hiçbir işlemci zamanı
tüketmiyordu ama bu askıya alma programcının kontrolünün dışındaydı. disariyi_bekle ise
kendi askıya alınmasını kendi yazdığı .await satırıyla tarif ediyor; yürütücü bu satırı
görmüyor bile, yalnız pollun döndürdüğü Pendingi görüyor. Askıya alınma kararı burada
kaynağın kendisinde, iş parçacığı örneğinde ise çalışma zamanının bir kararında duruyor.
Sınırlayıcı Ölçüm
Duraklama noktası kaynakta yazılı, ama ne kadar duraklayacağı yazılı değil. ic.await
satırı, disariyi_beklenin orada durabileceğini söylüyor; kaç poll çağrısı sonra devam
edeceği, icnin kendi mantığına bağlı ve bu ders yalnız sayıyı basıyor, süreyi değil.
BirKezDuraklar bir kez duruyor çünkü öyle yazıldı; başka bir gerçekleştirim on kez durabilir,
kaynaktaki .await görünümü değişmez. Duraklama noktasının görünürlüğü, duraklamanın var
olduğunu kaynaktan okumayı sağlıyor; kaç kez tekrarlanacağını değil.
Bu, kursun ortama bağlı veri yasağının bir sonraki dersteki I/O örneğinde de karşımıza çıkacak biçimi: modellenen bir bekleme, gerçek bir gecikmenin süresini değil, yalnız kaç kez yoklandığını ölçülebilir kılıyor.
Aynı sınır, iki ardışık duraklama noktasının ölçümünde de geçerli: n+1 kuralı yalnız
duraklama noktası sayısını sayıyor, her duraklamanın ne kadar sürdüğünü değil. ic1 ile
ic2 arasında gerçek bir programda saniyeler geçebilir ya da hiç geçmeyebilir — kural
değişmez, çünkü kural yoklama çağrılarının kaç kez yapıldığını, aralarında ne kadar
zaman geçtiğini değil.
Özet
async fn, bir gövdeyi elleFuturegerçekleştirmek yerine derleyicinin otomatik ürettiği bir durum makinesine çevirir;.awaitbu makinedeki duraklama noktasını işaretler.- Bir kez duraklayan gövde iki yoklamada, hiç duraklamayan gövde bir yoklamada tamamlanır; yoklama sayısı belirlenimcidir ve beş ayrı koşumda değişmez.
- İçteki bir geleceği bekleyen gövde de yoklanınca sınıfına girer, ama o gövdenin kendi tamamlanma noktası ilerlemek için başkası bitince sınıfına girer — ikisi aynı görev içinde bir arada bulunabilir.
- Go’nun Eşzamanlılık kursu bir birimin nasıl duracağını (bitiş) ölçtü; bu ders nerede duraklayıp nasıl devam ettiğini (duraklama ve devam) ölçüyor.
- Duraklama noktası kaynakta görünür yazılır, ama kaç kez tetikleneceği kaynaktan okunmaz —
yalnız koşumda ölçülür. Art arda gelen duraklama noktası, art arda gelen yoklama
gerektirir: son çağrı her zaman
Pendingdöndürmeyen, ilerlemeyi tamamlayan çağrıdır. - Bir
async fnin gövdesi çağrıldığı anda çalışmaz, yalnız derleyicinin ürettiği durum makinesinin ilk durumunu kurar — önceki dersteki tembellik kuralı burada da geçerlidir.
Sonraki Adım
Bu ders, bir gövdenin kaç kez duraklayıp devam ettiğini ölçtü; henüz sorulmayan soru, bu
duraklama ve devam çağrılarının kim tarafından zamanlandığı. kostur şimdiye kadar
verili bir araç olarak kullanıldı — kod her defasında aynı işlevi çağırdı ve onun uyandırılana
kadar beklediğini varsaydı. Sıradaki ders bu varsayımı sorguluyor ve kosturu doğrudan konu
ediniyor: aynı gövdeyi iki farklı yürütücü kurgusuyla koşturup, yürütücünün görevin gövdesine
hiçbir şey eklemediğini, yalnız ne zaman yoklayacağına karar verdiğini ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.