Ders 03 / 10
İş Parçacıkları Arası Güvenliğin Tipteki Karşılığı
Bir değerin bir iş parçacığı sınırını geçip geçemeyeceği tipte yazılı bir koşuldur: geçebilen bir tasarım gerçekten geçiriliyor ve kaydı okunuyor, ama tipin izin vermesi paylaşılan bir hesabın bağımsız varsayımla aynı sonucu vereceğini göstermiyor.
Yayın Güncelleme
Bu derste
- Bir değerin iş parçacığı sınırını geçebilmesinin derleme zamanında tipte denetlenen bir koşul olduğunu açıklar
- Geçebilen bir tasarımı koşturur ve geçemeyen bir tipin neden reddedildiğini düzyazıda tarif eder
- Tipin izin verdiği bir paylaşımın bağımsız varsayımdan farklı bir sonuç verebileceğini koşturarak gösterir
İçindekiler
Önceki ders bir değerin bir iş parçacığından diğerine geçebildiğini iki örnekle gösterdi: bir liste, bir ileti yapısı. İkisi de sorunsuzca geçti, ama bu geçişin kendisi hiç sorgulanmadı — sanki her tip bir kanaldan ya da bir paylaşılan kaba geçebilirmiş gibi davranıldı. Aslında geçemeyen tipler var ve bu izin çalışma zamanında değil derleme zamanında kontrol ediliyor. Bu dersin sorusu şu: bir tipin sınırı geçebilmesi neye bağlı ve bu koşul kaynakta nerede yazılı.
JVM, Eşzamanlılık ve Başarım kursu veri yarışı kavramını kurdu: iki iş parçacığının aynı belleğe eşzamanlı ve en az biri yazarak eriştiği, sonucun tanımsız olduğu durum. Bu kavram burada yeniden tanımlanmıyor. Rust’ın farkı, bu riski çalışma zamanında bir saptayıcıyla yakalamak yerine, riski taşıyan tipleri iş parçacığı sınırını geçmekten baştan alıkoyması. Bir değerin veri yarışına açık olup olmadığı, o değer paylaşılmadan önce, kaynak derlenirken biliniyor.
Send ve Sync: Tipte Yazılı Bir İzin
Sahiplik ve Bellek Modeli kursu sahiplik, ödünç alma ve referans kavramlarını kurdu, ama o
kurs bu kavramları tek bir iş parçacığı varsayımıyla anlattı. Bir değerin başka bir iş
parçacığına taşınabilmesi ile bir değere başka bir iş parçacığından referans
verilebilmesi, ayrı iki koşuldur ve Rust bu ikisini iki ayrı tipe bağlıyor: Send ve
Sync. Bir tip Send ise, o tipten bir değerin sahipliği başka bir iş parçacığına
güvenle taşınabilir. Bir tip Sync ise, o tipe bir referans (&T) başka bir iş
parçacığından güvenle okunabilir — yani T, Sync ise &T de Senddir.
Bu ikisi çevrilmeyen tip adlarıdır ve çoğu tip için derleyici tarafından otomatik
kurulur: bir yapının bütün alanları Send ise yapının kendisi de Senddir, aynı kural
Sync için de geçerli. Programcı bu iki tipi genellikle hiç yazmaz; yalnızca bir değeri bir
iş parçacığı sınırının üzerinden geçirmeye çalıştığında, derleyici o değerin tipinin bu
izne sahip olup olmadığını kontrol eder. İzin yoksa kaynak derlenmez — ama bu kısıt bu
derste bir hata olarak koşturulmuyor, yalnızca düzyazıda tarif ediliyor.
Bu otomatik kurulma, önceki iki dersin örneklerinin neden hiç sorgulanmadan geçtiğini de
açıklıyor. Önceki dersteki Vec<i32> ve Ileti yapısı — bir dizge ve bir tam sayı alanı
taşıyan basit bir yapı — hem Send hem Sync. Bunun nedeni, ikisinin de yalnızca temel
tiplerden ve standart kitaplığın kendisi Send/Sync işaretli koleksiyonlarından kurulmuş
olması. Bir tipin bu izinlerden birini kaybetmesi için, içinde özel bir paylaşım
düzeneği barındırması gerekiyor — tam da bir sonraki bölümde görülecek Rc ve RefCell
gibi. Derleyici bu izni tip tanımına bakarak çıkarıyor; programcı Send ya da Sync
sözcüğünü kaynakta hiç yazmasa da, her struct ve enum bildirimi örtük olarak bu iki
sınıflandırmadan birine giriyor.
Geçebilen Tasarım
Sahiplik ve Bellek Modeli kursunun Atomik Referans Sayımı dersi Arcı tanıttı, ama o
derste ölçülen yalnızca sayacın atomik olup olmamasıydı — sınır geçme sorusu orada açıkça
dışarıda bırakılmıştı. Bu ders o sınırı ölçüyor: Arcın atomik sayacı, onu hem Send hem
Sync yapan koşulun ta kendisi. İçindeki tip de bu iki izne sahipse (Mutex<T> her zaman
Senddir ve içindeki T, Send ise Mutex<T> de Syncdir), Arc<Mutex<T>> bütünüyle
iş parçacığı sınırını geçebilen bir tip oluyor.
Aşağıdaki program, alanlarının hepsi Send olan bir yapıyı Arc<Mutex<...>> içine
yerleştirip iki iş parçacığı arasında paylaşıyor. Arc hem Send hem Syncdir (içindeki
tip de öyleyse), Mutex de aynı şekilde — bu yüzden kayit değeri her iki iş parçacığına
da klonlanan bir tutamaçla güvenle geçebiliyor.
// sinir.rs — bir degerin is parcacigi sinirini gecebilmesi tipte yazili bir kosuldur.
use std::sync::{Arc, Mutex};
use std::thread;
struct PaylasilanKayit {
etiket: String,
deger: i32,
}
fn main() {
let kayit = Arc::new(Mutex::new(PaylasilanKayit {
etiket: String::from("olusum"),
deger: 5,
}));
let k1 = Arc::clone(&kayit);
let birinci = thread::spawn(move || {
let mut icerik = k1.lock().unwrap();
icerik.deger += 1;
icerik.etiket = String::from("birinci-yazdi");
});
birinci.join().unwrap();
let k2 = Arc::clone(&kayit);
let ikinci = thread::spawn(move || {
let icerik = k2.lock().unwrap();
(icerik.deger, icerik.etiket.clone())
});
let (okunan_deger, okunan_etiket) = ikinci.join().unwrap();
println!("sinir gecti — deger: {}, etiket: {}", okunan_deger, okunan_etiket);
}
sinir gecti — deger: 6, etiket: birinci-yazdi
IP4. birinci iş parçacığı kilit boşken çağrıldığı için kimseye ihtiyaç duymadan
ilerliyor — kendi başına sınıfına giriyor. ikinci iş parçacığı da aynı şekilde başlıyor,
ama onun asıl bağımlılığı birinci.join() çağrısının ana akışta önce tamamlanmış olması:
program ikinci klonu göndermeden önce ilk iş parçacığını birleştiriyor, böylece okunan
değerin birincinin yazdığı değer olduğu güvenceye bağlanıyor. Kayıt tipinin sınırı
geçebilmesi, bu iki iş parçacığının aynı veriye sırayla erişebilmesinin ön koşulu; koşulun
kendisi çalışma zamanında değil, Arc<Mutex<PaylasilanKayit>> yazıldığı anda, derleme
zamanında sağlanıyor.
Arc::clone çağrısının kendisi de dikkat çekici: k1 ve k2, aynı Mutexe işaret eden
iki ayrı tutamaç, ama ikisi de aynı öbek nesnesini paylaşıyor. Bu, önceki dersteki kanal
tutamacının klonlanmasıyla aynı düzenek — Arc::clone da veriyi kopyalamıyor, yalnızca
paylaşılan sahiplik sayacını bir artırıyor. Aradaki fark, kanalın klonlanan ucunun bir
gönderim kapısı olması, Arcın klonlanan ucunun ise doğrudan paylaşılan veriye erişim
noktası olması. kayit.lock() çağrısı bu noktada devreye giriyor: Arc paylaşılan
erişimi mümkün kılıyor, Mutex ise o erişimi aynı anda yalnızca bir tarafa veriyor. İkisi
ayrı sorunları çözüyor — biri “kaç tarafın bu veriye referansı var” sorusunu, öbürü “şu anda
kim yazabilir” sorusunu.
Geçemeyen Tip: Neden Rc<RefCell<..>> Sınırı Aşamaz
Sahiplik ve Bellek Modeli kursunun paylaşımlı sahiplik payı Rc ve RefCell çiftini tek iş
parçacıklı bir bağlamda tanıttı: Rc, bir değerin birden fazla sahibi olmasını sayarak
izliyor, RefCell ise ödünç kurallarını derleme yerine çalışma zamanında denetliyor. Bu
ikisi birlikte kullanıldığında Rc<RefCell<T>>, aynı sinir.rsdeki Arc<Mutex<...>> gibi
paylaşılan ve değiştirilebilir bir kap gibi görünüyor — ama bu kap bir iş parçacığı
sınırını geçemez.
Sebep sayaçların kendisinde. Rcnin tuttuğu referans sayısı, iki iş parçacığı aynı anda
artırıp azaltabilseydi bir veri yarışına açık olurdu, çünkü bu sayaç atomik değildir; aynı
şekilde RefCellin çalışma zamanı ödünç sayacı da atomik değildir. Derleyici bu iki tipin
ikisini de Send olarak işaretlemez — bu, bir eksiklik değil, tam olarak veri
yarışının önlenmesinin kaynağıdır. Bir programcı Rc<RefCell<PaylasilanKayit>> değerini
thread::spawne vermeye çalışsaydı, kaynak derlenmezdi; bu redd burada bir hata metniyle
gösterilmiyor, yalnızca nedeni anlatılıyor. Kabul edilen karşılığı zaten yukarıda koşturuldu:
aynı ihtiyaç, sayaçları atomik olan Arc ve kilitleme ile koruma sağlayan Mutex ile
karşılanıyor. İkisi arasındaki fark yalnız isim değil — Arcnin sayacı atomik işlemlerle
güncellenir, Mutex da erişimi sırayla verir; ikisi birlikte, Rc<RefCell<..>>in tek iş
parçacıklı dünyada üstlendiği rolü, birden fazla iş parçacığının bulunduğu bir dünyada
üstleniyor.
Bu ayrım, tek başına Mutex kullanmadan da fark ediliyor. Bir tipin yalnızca RefCell
taşıması (paylaşımlı sahiplik olmadan) onu Sync olmaktan çıkarır ama Send olmaktan
çıkarmaz — bir RefCell<T> başka bir iş parçacığına tek başına taşınabilir, çünkü o iş
parçacığı taşındıktan sonra tek sahip olur; ama iki iş parçacığı arasında paylaşılan bir
referansla kullanılamaz, çünkü çalışma zamanı ödünç sayacı korumasız kalır. Rc<T> ise
ikisini de kaybeder, çünkü Rcnin var oluş nedeni zaten paylaşılan sahipliktir ve o
paylaşım iş parçacıkları arasında hiçbir zaman güvenli değildir. Bu üç örnek — sade
RefCell, sade Rc ve ikisinin birleşimi — aynı kökten gelen üç farklı redde işaret
ediyor: iç değişebilirlik, referans sayımı, ya da ikisinin toplamı; hangisi olursa olsun
kaynaktaki koşul aynı, atomik olmayan bir sayaç ya da atomik olmayan bir denetim
mekanizması.
Bu redd derleme hatasıyla gösterilmiyor olsa da, sonucu Trait’ler, Genellikler ve Hata Yönetimi kursunda kurulan bir kalıba benziyor: orada da bir tipin belirli bir sınırı sağlamadığı durumlar kod bloğuna konmadan, yalnızca gerekçesiyle anlatılmıştı. Buradaki fark, orada ölçülen sınırların genellik (bir tip parametresinin hangi trait’leri sağlaması gerektiği) üzerine olması, burada ölçülenin ise iş parçacığı sınırını geçme izni olması — ikisi de derleyicinin bir tipin ne yapabileceğine dair kararını, kodu hiç çalıştırmadan verdiği örnekler.
Sınırlayıcı Ölçüm
Tipin bir paylaşıma izin vermesi, o paylaşımı kullanan programın doğru olduğu anlamına
gelmiyor. Arc<Mutex<i32>> veri yarışını engeller — aynı anda iki iş parçacığı aynı tam
sayıyı değiştiremez — ama hangi işlemin hangi sırayla uygulandığı hâlâ programın kendi
tasarımına bağlıdır. Aşağıdaki ölçüm bunu doğrudan gösteriyor: iki iş parçacığının her biri
paylaşılan bir sayıyı bağımsızca değiştirdiğini varsayarsa beklenen toplam bir değerdir; ama
aynı sayının üzerinde sırayla çalıştıklarında sonuç başka bir değer olur.
Bu, kilitli bir kaynağı paylaşan iki iş parçacığının karşılaşabileceği sorunlardan yalnızca biri; bir başkası, iki iş parçacığının birbirinin tuttuğu kilidi beklemesi durumunda ortaya çıkabilir ve bu kurs böyle bir durumu hiç koşturmuyor, çünkü sonucu bir kilitlenmedir ve belirlenimci bir çıktı üretmez. Burada ölçülen daha sınırlı ve daha temel bir soru: kilidin kendisi doğru çalışsa bile — hiçbir kilitlenme olmasa, hiçbir veri yarışı olmasa bile — paylaşılan bir değer üzerinde çalışan iki işlemin sırası, o işlemlerin sonucunu değiştirir mi. Yanıt evet ve bu, tipin verdiği güvencenin sınırını çiziyor.
// sinirlayici.rs — tip paylasima izin verir ama sira bagimliligi yanlis sonuc verebilir.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let baslangic = 5;
// bagimsiz varsayimla beklenen: A degeri ikiye katlar (10), B kendi payinda 10 ekler (15),
// ikisi ayri calissaydi toplam 10 + 15 = 25 olurdu.
let bagimsiz_beklenen = (baslangic * 2) + (baslangic + 10);
let paylasilan = Arc::new(Mutex::new(baslangic));
let p1 = Arc::clone(&paylasilan);
let a = thread::spawn(move || {
let mut deger = p1.lock().unwrap();
*deger *= 2;
});
a.join().unwrap();
let p2 = Arc::clone(&paylasilan);
let b = thread::spawn(move || {
let mut deger = p2.lock().unwrap();
*deger += 10;
});
b.join().unwrap();
let gercek_sonuc = *paylasilan.lock().unwrap();
println!("bagimsiz varsayimla beklenen: {}", bagimsiz_beklenen);
println!("paylasilan mutex ile gercek sonuc: {}", gercek_sonuc);
println!(
"tip paylasima izin verdi, sonuc bagimsiz varsayimla ayni mi: {}",
gercek_sonuc == bagimsiz_beklenen
);
}
bagimsiz varsayimla beklenen: 25 paylasilan mutex ile gercek sonuc: 20 tip paylasima izin verdi, sonuc bagimsiz varsayimla ayni mi: false
IP5. Beklenen değer 25, iki işlemin birbirinden habersiz, kendi başlangıç değeri
üzerinde çalıştığı varsayımıyla hesaplanıyor: A beşi ikiye katlayıp on buluyor, B beşe on
ekleyip on beş buluyor, toplam yirmi beş. Gerçek koşumda ise ikisi aynı sayıyı
paylaşıyor ve program Ayı Bden önce birleştiriyor: A beşi ikiye katlayıp ona
çıkarıyor, B bu ona on daha ekleyip yirmiye çıkarıyor. Sonuç yirmi, yirmi beş değil — ve bu
fark üç ayrı koşumda birebir aynı çıkıyor, çünkü birleştirme çağrıları sırayı belirlenimci
kılıyor. Mutex<i32> tipi bu paylaşıma izin verdi ve veri yarışını da gerçekten engelledi;
engellemediği şey, programcının “bu iki işlem birbirinden bağımsızdır” varsayımının doğru
olup olmadığı. Hangi işlemin önce kilit alacağı hâlâ programın tasarımına bağlı bir
sorudur ve rastgele hangisinin kazanacağı bu derste hiç ölçülmüyor — yalnızca sabit bir
sıraya (önce A, sonra B) zorlanan bu belirli tasarımın sonucu ölçülüyor.
Bu ölçümün asıl öğrettiği, “tip güvenlidir” ile “program doğrudur” iddialarının farklı
şeyler olduğu. Mutex<i32> etrafındaki tip sistemi, iki iş parçacığının aynı tam sayıyı
aynı anda değiştirmesini engeller — bu güvence her koşumda geçerlidir ve hiç bozulmaz. Ama
kilidin hangi sırayla verileceği, hangi işlemin diğerinden önce çalışacağı, tamamen
programın kendi tasarımına bağlıdır ve tip sistemi bu konuda hiçbir şey söylemez. Muteks ve
Okuma/Yazma Kilidi dersi bu boşluğu doğrudan konu edinecek: bir tarafın kilidi ne zaman
bekleyeceği ve bu beklemenin ilerleme sınıflarından hangisine düştüğü.
Özet
Sendbir tipin sahipliğinin başka bir iş parçacığına taşınabildiğini,Syncbir tipe referansın başka bir iş parçacığından okunabildiğini söyler; ikisi de derleme zamanında denetlenir.- Alanlarının hepsi
Send/Syncolan bir yapı,Arc<Mutex<...>>içine konduğunda sınırı geçebiliyor; ölçümdebirincikendi başına ilerledi,ikincibirincinin birleştirmesi tamamlanana kadar bekledi. Rc<RefCell<..>>sınırı geçemez, çünkü ikisinin de sayacı atomik değildir; kabul edilen karşılığıArc(atomik sayaç) veMutex(sıralı erişim) birlikte sağlıyor.- Tipin bir paylaşıma izin vermesi veri yarışını engeller, ama programın doğru sonucu vereceğini garanti etmez — sıraya bağımlı bir hesap, bağımsız varsayımdan farklı bir sonuç üretebilir.
- Hangi işlemin kilidi önce alacağı bu derste ölçülmüyor; yalnızca sabit bir sıraya zorlanan belirli bir tasarımın belirlenimci sonucu ölçülüyor.
Sonraki Adım
Bu derste iki iş parçacığı aynı veriye sırayla erişti, çünkü program birleştirme çağrılarıyla
sırayı kendisi zorladı: birinci tamamen bitmeden ikinci hiç başlamadı. Ama birleştirme
her zaman uygun bir araç değil — bazen iki iş parçacığının aynı anda çalışıyor olması
gerekiyor ve yalnızca paylaşılan veriye erişim anında sıraya girmesi isteniyor. Bu durumda
bir iş parçacığının diğerinin kilidi bırakmasını beklemesi gerekiyor ve bu bekleme
birleştirmeden farklı bir düzenek. Sıradaki ders bu beklemeyi doğrudan ölçüyor: kilidi
tutan kendi başına ilerlerken, kilidi bekleyen başkası bitince nasıl ilerliyor — ve okuma
amaçlı bir kilidin, yazma amaçlı bir kilitten neden daha gevşek bir kural taşıdığı.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.