İçeriğe geç
academia.sh

Ders 06 / 10

Gelecekler (futures)

Bir gelecek, kurulduğu anda değil yalnız yoklandığında ilerleyen tembel bir değerdir: aynı kaynaktan kurulan iki görevden yürütücüye verilen bir kaydı deftere yazarken, verilmeyen hiçbir iz bırakmaz.

Yayın Güncelleme

Bu derste

  • Bir geleceğin kurulmasının onu ilerletmediğini, yalnız yoklandığında ilerlediğini gösterir
  • Aynı kaynaktan kurulan iki görevi yürütücüye vererek ve vermeyerek yoklanınca ile hiç sınıflarını ayırt eder
  • Bu kursun sorduğu "ilerlemek için neye ihtiyaç var" sorusunun bir gövdenin kaç kez koştuğunu sayan sorudan farkını açıklar
İçindekiler

İş Parçacıkları konusu paylaşılan durumu bir kilitle ya da atomik bir işlemle koruyan iş parçacıklarını ölçtü; ikisinde de birim bir kez başlatıldıktan sonra ya kendi başına ilerliyordu ya da bir başkasının bitmesini bekliyordu. Bu konu aynı soruyu — bir iş ilerlemek için neye ihtiyaç duyuyor — başka bir birime taşıyor: eşzamansız görev. İlk bakışta yanıt tanıdık görünüyor, kendi başına ya da başkası bitince. Ama Rust’ın eşzamansız modeli üçüncü bir olasılığı gündeme getiriyor ve bu ders tam olarak onu kuruyor: bir görev, kaynakta yazılmış olmasına rağmen hiç ilerlemeyebilir.

Bir iş parçacığı, işletim sisteminin ayırdığı bir yürütme birimidir: thread::spawn çağrıldığı anda kendi yığıtı ve zamanlanma hakkı olan bir şey doğar, çağıran kod bir daha elini sürmese bile ilerlemeye devam eder. Bir eşzamansız görev bunun tam tersi bir şeyi temsil ediyor — kendi başına hiçbir yürütme bağlamı taşımayan, salt bir veri değeri. Bir HemenHazir örneği kurulduğunda ne bir yığıt ayrılıyor ne de bir zamanlanma hakkı doğuyor; ortada yalnız birkaç alan taşıyan sıradan bir yapı örneği var. Bu farkı görmek, kursun geri kalanı için belirleyici: iş parçacıkları konusu ilerlemeyi işletim sisteminin bir kararı olarak ölçtü, bu konu ilerlemeyi kaynaktaki bir kod parçasının kararı olarak ölçüyor.

Rust’ta bir eşzamansız görev, standart kitaplığın Future trait’ini gerçekleştiren bir değerdir. Bu trait tek bir zorunlu yöntem bildirir: poll. Yöntem bir kez çağrıldığında iki şeyden birini döndürür — görev bitmişse taşıdığı değeri saran Ready, bitmemişse Pending. Future trait’i kavram olarak Rust’a özgü değil; farkı, bu kararı kimin verdiğidir. Değeri üreten kod kendi kendine ilerlemez, ilerlemesi için dışarıdan bir çağrının pollu tetiklemesi gerekir. Bu çağrıyı yapan bileşene yürütücü denir ve bu konunun üçüncü dersi doğrudan ona ayrılmıştır; burada yürütücü yalnız görevi ilerletmek için gereken bir araç olarak kullanılıyor, kendisi bu dersin konusu değil.

Bu tembellik rastgele bir gerçekleştirim ayrıntısı değil, Future trait’inin tasarımının merkezinde duran bir karardır. Programlama Temelleri kursunda tanıtılan tembel değerlendirme kavramı, bir ifadenin değerinin yalnız gerçekten ihtiyaç duyulduğunda hesaplanmasını anlatıyordu; Rust’ın gelecekleri aynı ilkeyi eşzamansız işe uyguluyor. Bir gelecek kurmak yalnız bir tarif yazmaktır — hangi adımların hangi sırayla atılacağının bir kaydı. Bu tarifin gerçekten yürütülmesi, ayrı ve açık bir kararla, yürütücüye teslim edilerek başlıyor. Ayrım önemli çünkü aksi halde çok sayıda geleceği aynı anda kurmak bile pahalı olurdu: her biri kurulduğu anda çalışmaya başlasaydı, hangi görevin öncelikli olduğuna karar vermeden önce hepsi zaten yarı yolda olurdu. Tembellik, kurma ile çalıştırma arasına bir ayrım koyuyor; bu ayrım kursun kapanış dersinde bırakılan bir görevin bir daha hiç yoklanmamasının neden mümkün olduğunu da açıklıyor — kaynakta durup hiç ilerlememiş bir gelecek, zaten bu dersteki _hic_yoklanmayan ile aynı durumda.

Bir Değerin Kurulması Onu İlerletmez

Future gerçekleştiren bir tipin örneğini kurmak, sıradan bir yapı örneği kurmaktan farklı görünmüyor: alanlar dolduruluyor, değer bir değişkene bağlanıyor. Fark, bu kurulma anında hiçbir şeyin çalışmamasıdır. Aşağıdaki kesit, hazır olduğunda hemen tamamlanan en yalın gelecek türünü ve onu ilerletmek için gereken en yalın yürütücüyü tanımlıyor.

// ilerle.rs — cekirdegin kesiti: elle yazilan yurutucu ve hemen hazir olan bir 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;
            }
        }
    }
}

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)
    }
}

kostur işlevi, Rust’ın standart kitaplığında bulunmayan bir parçayı elle kuruyor: bir Waker, bir Context ve pollu tekrar tekrar çağıran bir döngü. Yürütücünün gövdesi bu derste yalnız bir araç olarak veriliyor; asıl ölçülen, HemenHazir tipinin kendisi. poll çağrılmadan önce bu tipin örneği bellekte duran sıradan bir değerden farksız — hiçbir alanı çalışmıyor, hiçbir kanala bir şey göndermiyor. Yalnız poll çağrıldığında kanal.send tetikleniyor ve değer üretiliyor.

kosturun döngüsü üç adımı tekrarlıyor: yoklama sayacını artırmak, pollu çağırmak, sonucu değerlendirmek. Ready dönerse döngü biter ve değerle birlikte toplam yoklama sayısı geri veriliyor. Pending dönerse döngü, Uyandiricinin taşıdığı mantıksal bayrağı false bulduğu sürece bir koşul değişkeninde bekliyor — bu, iş parçacığını meşgul bir döngüde döndürmek yerine gerçekten uyutan, Muteks ve Okuma/Yazma Kilidi dersinde tanıtılan kilitleme ilkelleriyle aynı ailenin bir üyesi. while döngüsü tek bir if yerine kullanılıyor çünkü bir koşul değişkeni kendisine hiç uyandırma çağrısı gelmeden de uyanabilir; bayrağı tekrar kontrol etmek bu olası yanlış uyanışı görmezden geliyor. Waker, bir Arc<Uyandirici>nin kopyası olarak taşınıyor — bir görev tek bir koşumda birden fazla kez Pending dönebildiği için, aynı Uyandirici birden fazla poll çağrısı arasında paylaşılabiliyor olmalı.

Aşağıdaki program aynı kaynaktan — aynı HemenHazir tanımından — iki görev kuruyor. Biri kostura veriliyor, öbürü yalnız bir değişkene bağlanıp bir daha hiç dokunulmuyor.

// tembel.rs — ayni kaynaktan iki gorev: biri yurutucuye verilir, oburu hic dokunulmaz.
mod ilerle;
use ilerle::{kostur, HemenHazir};
use std::sync::mpsc::channel;

fn main() {
    let (gonder, al) = channel::<String>();

    // ayni kaynaktan (HemenHazir tanimindan) iki ayri gorev kuruluyor
    let yoklanan = HemenHazir {
        ad: "gorev-yoklanan",
        kanal: gonder.clone(),
    };
    let _hic_yoklanmayan = HemenHazir {
        ad: "gorev-hic-yoklanmayan",
        kanal: gonder.clone(),
    };

    // yalniz biri yurutucuye veriliyor
    let (deger, yoklama) = kostur(yoklanan);

    drop(gonder);
    let kayitlar: Vec<String> = al.try_iter().collect();

    println!("donen deger: {}", deger);
    println!("yoklama sayisi: {}", yoklama);
    println!("kayit sayisi: {}", kayitlar.len());
    for k in &kayitlar {
        println!("  {}", k);
    }
}
donen deger: gorev-yoklanan
yoklama sayisi: 1
kayit sayisi: 1
  gorev-yoklanan

AS1. Defterde tek bir kayıt var. _hic_yoklanmayan değişkeni kaynakta yazılı, tipi HemenHazir, alanları dolu — ama karşılığında hiçbir satır basılmadı, çünkü poll hiç çağrılmadı. Bu iki görev arasındaki tek fark yürütücüye verilip verilmediği; kaynak kodları, tipleri ve alan değerleri birebir aynı yapıdan geliyor. Kayıt sayısı beş ayrı koşumda birebir aynı kaldı: bu bir yarış değil, bir davranış.

Konunun paylaştığı ortak ölçüm çekirdeği on üç birimi aynı dört sınıfa ayırıyor: beşi kendi başına, üçü yoklanınca, üçü başkası bitince ilerliyor, ikisi hiç ilerlemiyor. Bu dersteki iki birim — gorev-yoklanan ve gorev-hic-yoklanmayan — o on üçlünün yoklanınca ve hiç sınıflarından birer örnek; kalan sınıflar İş Parçacıkları konusunda ve bir sonraki derste ele alınıyor.

Bu, kursun sorduğu sorunun ilk yanıtı. gorev-yoklanan yoklanınca ilerledi: kendiliğinden değil, kosturun onu çağırmasıyla. gorev-hic-yoklanmayan ise hiç ilerlemedi — kaynakta durdu, hiçbir zaman poll almadı. Trait’ler, Genellikler ve Hata Yönetimi kursu bir gövdenin kaç kez koştuğunu saymıştı — her seferinde, yalnız gerektiğinde ya da hiç. Bu kurs aynı üçlüden farklı bir soru soruyor: kaç kez koştuğu değil, koşması için kimin onu ilerlettiği. gorev-yoklananı ilerleten özne kostur; HemenHazir tipinin kendisi bu konuda hiçbir karar vermiyor, yalnız çağrıldığında ne yapacağını söylüyor.

Bir Görev de bir Değer Üretir, Sıradan bir Çağrı Değildir

Sıradan bir fonksiyon çağrısı (f(x)) çağrıldığı anda çalışır; dönüş değeri geri gelene kadar çağıran taraf bekler. HemenHazirin poll yöntemi de benzer görünüyor — çağrıldığı anda sonucunu veriyor — ama bu, Future trait’inin genel kuralı değil, HemenHazire özgü bir durum. Genel kural şu: bir gelecek, poll çağrıldığında ya biter (Ready) ya da henüz bitmediğini söyler (Pending) ve kendini bir sonraki poll çağrısına bırakır. HemenHazir her zaman ilk çağrıda bitiyor çünkü beklediği hiçbir şey yok; bir sonraki derste kurulacak gelecek türü ise en az bir kez Pending dönüyor. Bu iki uç, aynı trait’in iki farklı gerçekleştirimi — sözleşme aynı, davranış farklı.

pollun imzası da bu farkı taşıyor: fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>. Context, yürütücünün geleceğe verdiği tek kanal — içinde bir Waker taşıyor ve gelecek, kendini bir sonraki poll için hazır saydığında bu Wakeri çağırıp yürütücüye haber verebiliyor. HemenHazir bunu hiç kullanmıyor çünkü hiçbir zaman Pending dönmüyor; sonraki ders bu kanalı doğrudan kullanan bir gelecek kuruyor.

Wakerin kendisi de tembel bir sözleşme taşıyor: uyandırma çağrısı yapıldığında hiçbir kod anında çalışmıyor, yalnız yürütücüye “bu görevi tekrar yokla” diye bir işaret bırakılıyor. kosturdaki Uyandirici tipi bu işareti bir mantıksal bayrak ve bir koşul değişkeniyle tutuyor: uyandırma çağrısı geldiğinde bayrağı true yapıp bekleyen döngüyü uyandırıyor. Bir gelecek Pending döndürüp bu çağrıyı hiç yapmazsa, yürütücü sonsuza kadar bekler — bu, geleceğin kendi sorumluluğu, yürütücünün değil. HemenHazir bu riski taşımıyor çünkü hiçbir zaman Pending dönmüyor; bir sonraki derste kurulacak gelecek türü, her Pending dönüşünde uyandırma çağrısını yaparak bu sözleşmeyi doğru yerine getiriyor.

kostur, geleceği Box::pin ile öbeğe yerleştiriyor. Bunun nedeni HemenHazirin kendisinde yok — bu basit tip yer değiştirdiğinde hiçbir sorun çıkarmaz. Neden, bir sonraki derste kurulacak async fn gövdelerinde ortaya çıkıyor: derleyici, bir async fni kendi içinde başka bir geleceği bekleyen bir durum makinesine çeviriyor ve bu makine kendi alanlarına işaret eden referanslar taşıyabiliyor. Böyle bir değer bellekte yer değiştirirse taşıdığı referanslar geçersiz kalır; Pin, bir değerin adresinin poll çağrıları arasında sabit kalacağını derleyiciye söyleyen bir garantidir. HemenHazir bu garantiye ihtiyaç duymuyor ama kostur her Future için aynı yolu izliyor, çünkü hangi geleceğin kendine referans taşıyıp taşımadığını genel bir işlev bilemez. Bir sonraki derste tanıtılacak async yazımı da tembellik kuralına uyuyor: eşzamansız bir fonksiyonu çağırmak onun gövdesini çalıştırmaz, yalnız bir gelecek değeri üretir — gövdenin gerçekten çalışması, tıpkı burada HemenHazirde olduğu gibi, o değerin yoklanmasını bekler.

Dikkat edilmesi gereken bir ayrıntı daha var: kosturun kendisi eşzamansız değil, sıradan bir fonksiyon çağrısı. main içindeki kostur(yoklanan) satırı çalıştığında, çağıran iş parçacığı gelecek bitene kadar bu çağrıda kalıyor — tıpkı İş Parçacıkları konusunda birleştirmeyi bekleyen ana iş parçacığı gibi. Eşzamansızlık burada iş parçacığı sayısını azaltmıyor; aynı iş parçacığı üzerinde birden fazla geleceği sırayla ya da iç içe ilerletmenin bir yolunu açıyor. Bu ayrım, ilerleyen derslerde birden fazla görevin aynı yürütücü üzerinde nasıl bir arada durabildiğini anlamanın temelini kuruyor.

poll Doğrudan da Çağrılabilir

kostur, pollu bir döngü içinde çağırıyor, ama bu çağrı biçimi zorunlu değil — poll sıradan bir yöntemdir ve elinde geçerli bir Context olan her kod onu çağırabilir. yoklanan.poll(&mut baglam) kosturun yerine doğrudan yazılsaydı, dönüş değeri yine Poll::Ready("gorev-yoklanan") olurdu — çünkü HemenHazir ilk çağrıda bitiyor, döngünün kaç kez çalıştığı onun için önemsiz. Yürütücünün asıl işi bu örnekte görünmüyor: tek bir çağrı yeten bir gelecekte yürütücü yalnızca bir aracı. Fark, bir sonraki derste kurulacak, birden fazla Pending dönen gelecek türünde ortaya çıkacak — orada pollu ne zaman tekrar çağıracağını bilmek, elle yazılan koddan yürütücünün sorumluluğuna geçiyor. Bu kursun sorduğu soru tam olarak burada devreye giriyor: bir görevi ilerleten şey kendi mantığı değil, onu çağıran kod.

Sınırlayıcı Ölçüm

Yoklanmayan bir görev bir hata değildir. _hic_yoklanmayan derleniyor, bellekte bir değişken olarak duruyor, kapsamı bitince bırakılıyor — ve bu sırada hiçbir hata, hiçbir uyarı üretmiyor. Derleyici, bir Futurein gerçekten yoklanıp yoklanmadığını izlemiyor; bunu izlemek programcının ya da yürütücünün işi. Bu, kursun ortama bağlı veri yasağından farklı bir tür sessizlik: burada eksik olan bir kayıt değil, hiç talep edilmemiş bir ilerlemedir. Kodun kendi davranışı, “hiç” sınıfının neden ayrı bir hata biçimi olmadığını gösteriyor — kaynakta yazılı bir şey, çalıştırılmadığı sürece hiçbir iz bırakmadan var olabiliyor.

Bu dersteki “hiç” örneği, konunun paylaştığı ortak ölçüm çekirdeğindeki iki “hiç” biriminden biriyle aynı ailede: biri hiç başlatılmayan bir kapanış, öbürü hiç yoklanmayan bir görev — ikisi de kaynakta yazılı, ikisi de derlendi, ikisi de hiçbir iz bırakmadı. İş Parçacıkları konusunda bir birim başlatılmadan kaldığında da aynı sessizlik gözlemleniyordu; farkı, orada sorumlu thread::spawnın hiç çağrılmaması, burada pollun hiç çağrılmamasıydı. İkisi de aynı kurala bağlanıyor: kaynakta yazılı olmak, ilerlemiş olmak anlamına gelmiyor.

Özet

  • Bir gelecek, Future trait’ini gerçekleştiren bir değerdir; tek zorunlu yöntemi poll, dönüşü Ready (bitti) ya da Pending (henüz bitmedi).
  • Bir geleceğin kurulması hiçbir şeyi çalıştırmaz — yalnız poll çağrısı ilerletir. Bu, bir görevin yoklanınca sınıfına girmesinin temelidir.
  • Aynı kaynaktan kurulan iki görevden yürütücüye verilmeyeni hiç ilerlemedi: kaynakta yazılıydı, derlendi, ama hiçbir kayıt üretmedi.
  • Bir görevi ilerleten özne yürütücüdür, görevin kendisi değil — bu, bir gövdenin kaç kez koştuğunu sayan Trait’ler, Genellikler ve Hata Yönetimi kursundan farklı bir eksendir.
  • Context ve Waker, bir geleceğin yürütücüye “hazır olduğumda haber ver” demesini sağlayan kanaldır; bu ders onu tanıttı, kullanımı bir sonraki derste.
  • Bir iş parçacığı işletim sisteminin ayırdığı bir yürütme birimidir, bir gelecek ise salt bir veri değeridir — kendi başına hiçbir yürütme bağlamı taşımaz, yalnız onu yoklayan yürütücü çağrıldığı sürece ilerler.

Sonraki Adım

Bu ders bir görevin kurulmasıyla ilerlemesi arasındaki farkı HemenHazir üzerinden gösterdi — hiç durmayan, ilk çağrıda biten bir gelecek. Bu, mümkün en yalın gelecek türüydü: tek bir poll çağrısı yetiyor, Pending hiç dönmüyor. Sıradaki ders aynı sözleşmeyi bir kez duran bir gelecekle genişletiyor: duraklama noktası kaynakta nasıl görünür oluyor, async/await yazımı bu noktaları nasıl kuruyor ve bir görevi bekleyen başka bir görev hangi sınıfa giriyor.

İ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