İçeriğe geç
academia.sh

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 n+1n+1 kuralıyla aynı aileden — orada nn ardışık duraklama noktası vardı, burada nn 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 Pending döner ve kendini uyandırır.
  • Bir kaynağın kaç yoklamada tamamlanacağı, kaynağın kendi kalan mantığı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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat