Ders 09 / 10
Eşzamansız Girdi/Çıktı
Girdi çıktı beklerken duraklama, gerçek bir ağ ya da dosya değil, hazır olmayınca Pending dönen bir kaynakla modellenir; ölçülen kaç yoklamada tamamlandığı, kaynağın hazır olma süresi değil.
Yayın Güncelleme
Bu derste
- Girdi çıktı beklerken duraklamanın, hazır olmayan bir kaynağı modelleyen bir gelecekle nasıl kurulduğunu gösterir
- Farklı hazır olma gecikmeleri taşıyan kaynakların yoklama sayılarını karşılaştırarak bu sayının kaynağın kendi mantığından geldiğini doğrular
- Standart Kütüphane ve Ağ Uygulamaları kursunun tip bilgisi üzerinden ölçtüğü sınırla bu dersin ilerleme koşulu üzerinden ölçtüğü sınır arasındaki farkı açıklar
İçindekiler
Önceki ders iki görev türünü inceledi: hiç duraklamayan ve kendi kararıyla bir kez duraklayan.
İkisinde de duraklamanın nedeni görevin kendi mantığıydı — dışarıdan hiçbir koşula bağlı
değildi. Bir programın gerçek dünyada beklediği şeylerin çoğu böyle değil: bir ağ bağlantısı
cevap vermeden önce belirsiz bir süre geçer, bir dosya sistemine gönderilen okuma isteği
donanımın hazır olmasını bekler. Bu ders, bu tür bir bekleyişi ele alıyor — ama kursun ortama
bağlı veri yasağı gereği gerçek bir ağ ya da dosya kullanmadan. Bekleme, hazır olmayan bir
kaynağın Pending döndüğü, hazır olduğunda kendini uyandırdığı bir modelle kuruluyor.
Gerçek bir ağ soketi ya da dosya tanıtıcısı kullanmamanın nedeni yalnız kolaylık değil.
Kursun başından beri sürdürdüğü belirlenimcilik kısıtı, koşumun hangi makinede çalıştırılırsa
çalıştırılsın aynı sayıyı vermesini istiyor. Gerçek bir ağ isteğinin ne kadar süreceği ağın
kendisine, gerçek bir dosya okumasının ne kadar süreceği depolama aygıtına bağlı — bu
kursun beş ayrı koşumda birebir aynı çıktı vermesini imkânsız kılardı. Bunun yerine,
gerçek dünyadaki “kaynak henüz hazır değil” durumunun soyut biçimi yakalanıyor: bir
poll çağrısının Pending dönmesi ve daha sonra bir Readyye dönüşmesi. Kaynağın neden
hazır olmadığı — bir ağ gecikmesi mi, bir disk erişimi mi — bu modelin dışında kalıyor;
modelin içinde kalan tek şey, hazır olana kadar kaç kez sorulacağı.
Hazır Olmayan Bir Kaynağı Modellemek
SahteKaynak, bir girdi/çıktı kaynağının en indirgenmiş hâli: kaç yoklama sonra hazır
olacağını bir sayaçla taşıyor. Her poll çağrısında sayaç sıfır değilse bir azalıyor,
Pending dönüyor ve kendini uyandırıyor; sıfıra ulaştığında kanala bir kayıt yazıp Ready
dönüyor.
// ilerle.rs — cekirdegin kesiti: hazir olmayan bir kaynagi modelleyen gelecek.
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;
}
}
}
}
// hazir olmayan bir girdi/cikti kaynagini modelliyor: her yoklamada bir yoklama kalan
// sayisini azaltiyor, kalan sifira dusunce hazir kabul edip degeri gonderiyor.
pub struct SahteKaynak {
pub kalan: u32,
pub ad: &'static str,
pub kanal: Sender<String>,
}
impl Future for SahteKaynak {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<&'static str> {
if self.kalan == 0 {
self.kanal.send(self.ad.to_string()).unwrap();
Poll::Ready(self.ad)
} else {
self.kalan -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
pub async fn veriyi_oku(kaynak: SahteKaynak, kanal: Sender<String>) -> &'static str {
let _ = kaynak.await;
kanal.send(String::from("okuma-tamamlandi")).unwrap();
"okundu"
}
SahteKaynak, önceki dersteki BirKezDurakların genellenmiş biçimi: BirKezDuraklar tam
olarak bir kez duraklıyordu, SahteKaynak kalan alanında yazan sayı kadar duraklıyor. Bu
sayı gerçek bir programda bir donanım kesmesinin, bir işletim sistemi bildiriminin ya da bir
ağ paketinin gelişinin karşılığı olurdu; burada yalnız bir tam sayı ve kaynağın davranışını
belirliyor — davranışını temsil ettiği gerçek olayı değil. veriyi_oku, bu kaynağı bekleyen
bir async fn: kaynak.await bitene kadar kendi de ilerlemiyor, tıpkı önceki derste içteki
bir geleceği bekleyen dış gövdenin yaptığı gibi.
SahteKaynakin poll yöntemi her Pending dönüşünde cx.waker().wake_by_ref() çağırıyor —
tıpkı BirKezDuraklar gibi. Bu, gerçek bir kaynakla arasındaki en önemli fark: gerçek bir ağ
soketi kendi kendini uyandırmaz, işletim sisteminin bir bildirim mekanizması (soket hazır
olduğunda çalışma zamanına haber veren bir alt sistem) Wakeri çağırır. SahteKaynak bu
dış bildirimi taklit ediyor — kendi zamanlayıcısı olmadığı için, kendini her seferinde hemen
uyandırıp bir sonraki poll çağrısında kalanı bir azaltmayı tercih ediyor. Sonuç, dışarıdan
bakıldığında aynı: yürütücü Pending görüyor, bir süre sonra tekrar yoklama fırsatı buluyor,
kaynak sonunda hazır oluyor.
Kaç Yoklama, Ne Kadar Bekleme
Aşağıdaki program üç farklı gecikmeyle kurulmuş kaynakları koşturuyor: biri kısa (bir yoklama
bekliyor), biri uzun (dört yoklama bekliyor), biri de bir async fn içinden bekleniyor.
// girdi-cikti.rs — hazir olmayan bir kaynagin kac yoklamada tamamlandigi.
mod ilerle;
use ilerle::{kostur, veriyi_oku, SahteKaynak};
use std::sync::mpsc::channel;
fn main() {
let (gonder, al) = channel::<String>();
let (_deger_a, yoklama_a) = kostur(SahteKaynak {
kalan: 1,
ad: "kaynak-kisa-bekleme",
kanal: gonder.clone(),
});
let (_deger_b, yoklama_b) = kostur(SahteKaynak {
kalan: 4,
ad: "kaynak-uzun-bekleme",
kanal: gonder.clone(),
});
let kaynak_c = SahteKaynak {
kalan: 2,
ad: "kaynak-ic",
kanal: gonder.clone(),
};
let (_deger_c, yoklama_c) = kostur(veriyi_oku(kaynak_c, gonder.clone()));
drop(gonder);
let mut kayitlar: Vec<String> = al.try_iter().collect();
kayitlar.sort();
println!("kisa bekleme (kalan=1) -> {} yoklamada tamamlandi", yoklama_a);
println!("uzun bekleme (kalan=4) -> {} yoklamada tamamlandi", yoklama_b);
println!("okuma, icteki kaynagi bekliyor -> {} yoklamada tamamlandi", yoklama_c);
println!();
println!("kayitlar ({}):", kayitlar.len());
for k in &kayitlar {
println!(" {}", k);
}
}
kisa bekleme (kalan=1) -> 2 yoklamada tamamlandi uzun bekleme (kalan=4) -> 5 yoklamada tamamlandi okuma, icteki kaynagi bekliyor -> 3 yoklamada tamamlandi kayitlar (4): kaynak-ic kaynak-kisa-bekleme kaynak-uzun-bekleme okuma-tamamlandi
AS5. Yoklama sayısı, kalan alanının başlangıç değerinden bire fazla: kalan: 1 iki
yoklamada, kalan: 4 beş yoklamada tamamlanıyor. Kural, Async/Await dersindeki kuralıyla
aynı aileden — orada ardışık duraklama noktası vardı, burada ardışık Pending dönüşü
var; ikisi de son çağrıyı, Ready dönen çağrıyı, sayıya dahil ediyor. kaynak-ic (kalan: 2), veriyi_oku içinden bekleniyor ve üç yoklamada tamamlanıyor — iki Pending artı bir
Ready, tıpkı doğrudan kostura verilseydi tamamlayacağı sayı. Bu beş ayrı koşumda birebir
aynı kaldı.
Bekleyen Görevin Sınıfı
kaynak-kisa-bekleme ve kaynak-uzun-bekleme yoklanınca sınıfına giriyor: ikisi de
kostura doğrudan veriliyor, ikisi de kendi kalan sayaçlarına göre ilerliyor. veriyi_oku
ise, önceki dersteki disariyi_bekle ile aynı konuma düşüyor — kendi ilerlemesi kaynak_cnin
tamamlanmasına bağlı, dolayısıyla başkası bitince sınıfına giriyor. kaynak_cnin kendisi
yine yoklanınca; iki sınıf burada da, önceki derste olduğu gibi, aynı görev ağacının farklı
katmanlarında bir arada bulunuyor.
Standart Kütüphane ve Ağ Uygulamaları kursu bir ağ sınırını geçen değerin tip bilgisini
neyin koruyup neyin kaybettiğini ölçmüştü — sınırın bir tarafındaki tip, öbür tarafa hangi
biçimde ulaşıyordu. Bu ders aynı sınırı başka bir eksenden ölçüyor: bir girdi/çıktı kaynağını
beklerken bir görevin ilerlemek için neye ihtiyaç duyduğu. Tip bilgisi burada hiç
tartışılmıyor — SahteKaynakin ürettiği değer zaten en baştan &'static str; sınır, bir
tipin biçimini değil, bir görevin ilerleme koşulunu ayırıyor.
İki ölçüm de aynı sınırı — bir programın kendi kontrolündeki kod ile dışarıdan gelen bir
kaynak arasındaki sınırı — konu ediniyor, ama sordukları soru farklı. Orada soru “bu sınırı
geçen değer hâlâ aynı tipte mi” idi; burada soru “bu sınırın öbür tarafındaki kaynak hazır
olana kadar bu taraf ne yapıyor” oluyor. kaynak-kisa-bekleme ile kaynak-uzun-bekleme
arasındaki fark bir tip farkı değil — ikisi de aynı SahteKaynak tipinden, ikisinin de
Outputu aynı; fark yalnız kalan alanının başlangıç değerinde, yani ilerleme koşulunun ne
kadar sıklıkla karşılanacağında.
Yürütücü Burada da Fark Etmiyor
Yürütücüler dersi, iki farklı yürütücü kurgusunun aynı görevi aynı sayıda yokladığını
göstermişti. Aynı sonuç, modellenen bir girdi/çıktı kaynağı için de geçerli. Aşağıdaki kesit,
önceki dersteki kostur_donguyu bu derse taşıyıp aynı SahteKaynaki iki farklı yürütücüyle
koşturuyor.
// ilerle.rs — cekirdegin kesiti degisti: onceki dersteki ikinci yurutucu (kostur_dongu)
// buraya taniniyor, ayni SahteKaynak iki yurutucuyle karsilastiriliyor.
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 BosUyandirici;
impl Wake for BosUyandirici {
fn wake(self: Arc<Self>) {}
}
pub fn kostur_dongu<F: Future>(gorev: F) -> (F::Output, u32) {
let mut gorev = Box::pin(gorev);
let u = Arc::new(BosUyandirici);
let waker: Waker = u.clone().into();
let mut baglam = Context::from_waker(&waker);
let mut yoklama = 0;
loop {
yoklama += 1;
if let Poll::Ready(x) = gorev.as_mut().poll(&mut baglam) {
return (x, yoklama);
}
}
}
pub struct SahteKaynak {
pub kalan: u32,
pub ad: &'static str,
pub kanal: Sender<String>,
}
impl Future for SahteKaynak {
type Output = &'static str;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<&'static str> {
if self.kalan == 0 {
self.kanal.send(self.ad.to_string()).unwrap();
Poll::Ready(self.ad)
} else {
self.kalan -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
// iki-yurutucu-io.rs — ayni modellenen kaynak, iki farkli yurutucu, ayni yoklama sayisi.
mod ilerle;
use ilerle::{kostur, kostur_dongu, SahteKaynak};
use std::sync::mpsc::channel;
fn main() {
let (gonder, al) = channel::<String>();
let (_deger_1, yoklama_1) = kostur(SahteKaynak {
kalan: 3,
ad: "kaynak-kostur",
kanal: gonder.clone(),
});
let (_deger_2, yoklama_2) = kostur_dongu(SahteKaynak {
kalan: 3,
ad: "kaynak-dongu",
kanal: gonder.clone(),
});
drop(gonder);
let mut kayitlar: Vec<String> = al.try_iter().collect();
kayitlar.sort();
println!("kostur={} kostur_dongu={} ayni mi: {}", yoklama_1, yoklama_2, yoklama_1 == yoklama_2);
println!("kayitlar ({}):", kayitlar.len());
for k in &kayitlar {
println!(" {}", k);
}
}
kostur=4 kostur_dongu=4 ayni mi: true kayitlar (2): kaynak-dongu kaynak-kostur
AS6. Aynı kalan: 3 değeriyle kurulan iki kaynak, iki bambaşka yürütücüyle koşturulduğunda
birebir aynı yoklama sayısını (4) veriyor. SahteKaynakin poll yöntemi hangi yürütücünün
onu çağırdığını hiç bilmiyor — yalnız kendi kalan alanına bakıyor. Bu, önceki dersteki
bulguyu modellenen bir girdi/çıktı kaynağına taşıyor: yürütücü, bir kaynağın ne kadar
bekleteceğine karışmıyor, yalnız beklerken çağıran iş parçacığının ne yaptığına karışıyor —
ve bu ikinci fark, kursun basmayı yasakladığı bir alanda kalıyor.
Sınırlayıcı Ölçüm
Modellenen bekleme, gerçek bir gecikmenin süresini göstermiyor. kalan: 4 alanı beş yoklama
gerektiriyor, ama bu beş çağrı arasında gerçekte hiçbir bekleme yok — kosturun koşul
değişkeni her wake_by_ref çağrısında hemen uyanıyor, çünkü SahteKaynak kendini her
Pending dönüşünde kendisi uyandırıyor. Gerçek bir kaynak (bir ağ soketi, bir dosya
tanıtıcısı) bu uyandırmayı kendi hazır olduğu anda, işletim sisteminin bir bildirimiyle
yapardı; burada bu bildirim kalan sayacının azalmasıyla taklit ediliyor. Ölçülen şey her
zaman yoklama sayısı — bekleme süresi değil, çünkü süre ortama bağlı ve bu kursun
başından beri basılmayan bir değer.
Bu sınırın bir sonucu da şu: kalan alanına yazılan sayı büyütülürse yoklama sayısı da
orantılı büyür, ama koşumun gerçek süresi ölçülebilir biçimde değişmez — hiçbir yerde gerçek
bir bekleme yok, yalnız döngünün kaç kez çalıştığı değişiyor. Gerçek bir programda bu ilişki
tersine döner: yoklama sayısı sabit kalabilir (bir yürütücü, bir kaynağı yalnız gerçekten
hazır olduğunda tekrar yoklayacak biçimde tasarlanabilir), ama gerçek bekleme süresi kaynağın
kendi hızına bağlı olarak değişir. Bu iki eksenin — yoklama sayısı ve bekleme süresi —
birbirinden bağımsız olabileceğini görmek, modelin neyi temsil edip neyi etmediğini anlamak
için gerekli: bu ders yalnız birincisini, kursun en başından beri basılmasına izin verilen
tek sayıyı ölçüyor.
Özet
- Girdi/çıktı beklerken duraklama, hazır olmayan bir kaynağı modelleyen bir gelecekle
kurulur: kaynak hazır değilse
Pendingdöner ve kendini uyandırır. - Bir kaynağın kaç yoklamada tamamlanacağı, kaynağın kendi
kalanmantığından gelir ve belirlenimcidir — beş ayrı koşumda birebir aynı kalır. - Doğrudan yoklanan bir kaynak yoklanınca sınıfına girer; onu bekleyen bir
async fnin kendi tamamlanması başkası bitince sınıfına girer. - Standart Kütüphane ve Ağ Uygulamaları kursu bir sınırı tip bilgisi üzerinden ölçtü; bu ders aynı sınırı bir görevin ilerleme koşulu üzerinden ölçer.
- Modellenen bekleme gerçek gecikmeyi göstermez; ölçülen şey yoklama sayısıdır, geçen süre değil.
- Aynı modellenen kaynak iki farklı yürütücüyle koşturulduğunda da aynı yoklama sayısını verir — Yürütücüler dersinin bulgusu, modellenen girdi/çıktıda da değişmeden geçerlidir.
Sonraki Adım
Bu konu, bir görevin ilerlemek için neye ihtiyaç duyduğunu dört adımda kurdu: bir görev
kurulmasıyla ilerlemez, yalnız yoklandığında ilerler; async/await bu yoklamaların nerede
duraklayıp nereden devam edeceğini kaynakta görünür kılar; bir yürütücü bu yoklamaların ne
zaman olacağına karar verir ama görevin ürettiği değere karışmaz; ve bir girdi/çıktı kaynağı
bu yoklama zincirini, hazır olma süresine bağlı bir sayıyla uzatabilir. Dört ders boyunca
ölçülen tek sayı — kaç kez poll çağrıldığı — her defasında kaynaktan önceden çıkarılabilir
kaldı.
Kalan soru, kaynakta yazılı ama hiç tamamlanmayan ya da tamamlanmadan bırakılan bir görevin ne
olacağı. Kursun kapanış dersi bir görevin bırakılmasıyla kurulan iptali ele alıyor:
bırakılan bir görev bir daha hiç yoklanmıyor ve bu dersin SahteKaynaki gibi kaynakta yazılı
kalıyor, ama Gelecekler dersindeki _hic_yoklanmayan ile aynı akıbeti paylaşıyor — hiç
ilerlemeden. Zaman aşımı da aynı çerçeveden, süreyle değil, yoklama sayısıyla modelleniyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.