Her Şeyi Birleştirmek: Future, Görev ve İş Parçacığı
- bölümde gördüğümüz gibi, iş parçacıkları eşzamanlılığa yaklaşmanın bir
yolunu sunar. Bu bölümde ise başka bir yol gördük:
async, future ve akışlar. Hangisini ne zaman seçeceğinizi merak ediyorsanız kısa cevap şu: duruma göre. Üstelik çoğu zaman seçim iş parçacıkları veyaasyncdeğil, iş parçacıkları veasyncolur.
Birçok işletim sistemi onlarca yıldır iş parçacığı temelli eşzamanlılık modelleri sunuyor; bu yüzden birçok programlama dili de onları destekliyor. Ama bunun bedelleri var. Pek çok işletim sisteminde her iş parçacığı kayda değer miktarda bellek kullanır. Ayrıca iş parçacıkları yalnızca işletim sistemi ve donanım destekliyorsa mümkündür. Masaüstü ve mobil sistemlerin aksine bazı gömülü sistemlerde işletim sistemi bile yoktur; dolayısıyla iş parçacıkları da yoktur.
async modeli farklı ve sonuçta tamamlayıcı bir ödünleşim seti sunar. async
yaklaşımında eşzamanlı işlemler için ayrı ayrı iş parçacıkları gerekmez.
Bunun yerine işlemler, akışlar bölümünde trpl::spawn_task ile yaptığımız gibi
görevler üstünde çalışabilir. Görev, iş parçacığına benzer; ama onu işletim
sistemi değil, kütüphane düzeyindeki kod, yani çalışma zamanı yönetir.
İş parçacığı başlatma API’leri ile görev başlatma API’lerinin birbirine benzemesinin iyi bir nedeni var. İş parçacıkları, senkron işlem kümeleri için bir sınır görevi görür; eşzamanlılık iş parçacıkları arasında mümkündür. Görevler ise eşzamansız işlem kümeleri için bir sınırdır; eşzamanlılık hem görevler arasında hem de görevlerin içinde mümkündür, çünkü bir görev gövdesindeki future’lar arasında geçiş yapabilir. Son olarak future’lar, Rust’ın en ince taneli eşzamanlılık birimidir ve her future başka future’lardan oluşan bir ağacı temsil edebilir. Çalışma zamanı, daha doğrusu onun yürütücüsü (executor), görevleri yönetir; görevler de future’ları yönetir. Bu yönüyle görevler, işletim sistemi yerine çalışma zamanı tarafından yönetilen hafif iş parçacıkları gibidir.
Bu, async görevlerin her zaman iş parçacıklarından daha iyi olduğu anlamına
gelmez; tersi de doğru değil. İş parçacıklarıyla eşzamanlılık, bazı bakımlardan
async ile eşzamanlılıktan daha basit bir programlama modelidir. Bu hem güçlü
yanı hem de zayıf yanı olabilir. İş parçacıkları çoğu zaman “başlat ve bırak”
gibidir; yerleşik bir future karşılıkları yoktur. İşletim sistemi müdahale
etmedikçe başladıkları işi sonuna kadar götürürler.
Öte yandan iş parçacıkları ile görevler çoğu zaman birlikte çok iyi çalışır;
çünkü bazı çalışma zamanlarında görevler iş parçacıkları arasında taşınabilir.
Hatta bu bölümde kullandığımız çalışma zamanı, spawn_blocking ve spawn_task
işlevleri dahil, varsayılan olarak çok iş parçacıklıdır. Birçok çalışma zamanı,
iş parçacıklarının o anki kullanımına göre görevleri şeffaf biçimde aralarında
taşıyan iş çalma (work stealing) yaklaşımını kullanır. Böylece sistemin genel
performansı iyileşir. Bu yaklaşım, hem iş parçacıklarını hem görevleri hem de
dolayısıyla future’ları birlikte gerektirir.
Hangi yöntemi nerede kullanacağınıza karar verirken şu pratik kuralları akılda tutabilirsiniz:
- İş çok iyi paralelleştirilebiliyorsa yani CPU-bağımlıysa, örneğin büyük bir veri kümesini bağımsız parçalara ayırıp işleyebiliyorsanız, iş parçacıkları daha iyi seçimdir.
- İş çok iyi eşzamanlıysa yani G/Ç-bağımlıysa, örneğin farklı kaynaklardan
farklı hızlarda gelen iletileri ele alıyorsanız,
asyncdaha iyi seçimdir.
Hem paralellik hem eşzamanlılık gerekiyorsa, iş parçacıkları ile async
arasında seçim yapmak zorunda değilsiniz. İkisini rahatça birlikte
kullanabilirsiniz. Böylece her biri en iyi olduğu rolü üstlenir. 17-25 numaralı
liste, gerçek Rust kodunda sık rastlanan böyle bir birleşime örnek gösteriyor.
extern crate trpl; // for mdbook test
use std::{thread, time::Duration};
fn main() {
let (gonderici, mut alici) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
gonderici.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::block_on(async {
while let Some(message) = alici.recv().await {
println!("{message}");
}
});
}
Önce bir eşzamansız kanal oluşturuyoruz. Sonra kanalın gönderici tarafının
sahipliğini move ile alan bir iş parçacığı başlatıyoruz. İş parçacığının
içinde 1’den 10’a kadar sayıları gönderiyor ve her birinin arasında bir saniye
bekliyoruz. Son olarak, bölüm boyunca yaptığımız gibi trpl::block_on içine
verilen bir async bloktan üretilen future’ı çalıştırıyoruz. Bu future içinde de
diğer mesaj iletimi örneklerinde olduğu gibi iletileri bekliyoruz.
Bölümün başındaki video örneğine geri dönersek, video kodlama görevlerini ayrı bir iş parçacığında çalıştırdığınızı düşünün; çünkü video kodlama CPU-bağımlı bir iştir. Ama bu işlemler bittiğinde kullanıcı arayüzünü bir eşzamansız kanal üzerinden bilgilendirebilirsiniz. Gerçek dünyada bu tür birleşimlerin sayısız örneği vardır.
Özet
Bu kitapta eşzamanlılık konusunu son kez görmüyorsunuz. 21. bölümdeki proje, buradaki küçük örneklerden daha gerçekçi bir senaryoda bu kavramları uygulayacak; ayrıca iş parçacıklarıyla çözüm üretmeyi görevler ve future’larla çözüm üretmeyle daha doğrudan karşılaştıracak.
Hangi yaklaşımı seçerseniz seçin, Rust size güvenli, hızlı ve eşzamanlı kod yazmak için gerekli araçları verir; ister yüksek trafiğe sahip bir web sunucusu yazın, ister gömülü bir işletim sistemi.
Sıradaki bölümde, Rust programları büyüdükçe problemleri modellemenin ve çözümleri düzenlemenin deyimsel yollarını konuşacağız. Ayrıca Rust’ın deyişlerinin nesne yönelimli programlamadan aşina olabileceğiniz kalıplarla nasıl ilişki kurduğunu da ele alacağız.