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,
Futuretrait’ini gerçekleştiren bir değerdir; tek zorunlu yöntemipoll, dönüşüReady(bitti) ya daPending(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.
ContextveWaker, 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.