İçeriğe geç
academia.sh

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 nn duraklama noktası, art arda gelen n+1n+1 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 elle Future gerçekleştirmek yerine derleyicinin otomatik ürettiği bir durum makinesine çevirir; .await bu 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 nn duraklama noktası, art arda gelen n+1n+1 yoklama gerektirir: son çağrı her zaman Pending dö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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat