Ders 04 / 10
Muteks ve Okuma/Yazma Kilidi
Bir dışlama kilidini tutan kendi başına ilerler, onu bekleyen başkası bitince ilerler; bu sıra bir kanal el sıkışmasıyla belirlenimci kılınır ve okuma/yazma kilidinin okuma tarafı, aynı anda birden fazla okuyucuya izin vererek dışlama kilidinden ayrılır.
Yayın Güncelleme
Bu derste
- Kilidi tutan tarafın kendi başına, kilidi bekleyen tarafın başkası bitince ilerlediğini bir kanal el sıkışmasıyla belirlenimci biçimde ölçer
- İki bekleyen taraf arasında hangisinin önce gireceğinin ölçülmediğini ve neden ölçülmediğini gösterir
- Okuma/yazma kilidinin okuma tarafının aynı anda birden fazla okuyucuya izin verdiğini koşturarak doğrular
İçindekiler
Önceki ders paylaşılan bir tam sayı üzerinde iki iş parçacığının sırayla çalıştığını, ama bu
sırayı birleştirme çağrılarıyla kendisinin zorladığını gösterdi: birinci tamamen bitmeden
ikinci hiç başlamadı. Bu iki işlemin aynı anda çalışıp yalnızca paylaşılan veriye erişim
anında sıraya girmesi gerektiğinde, birleştirme yeterli bir araç değil — iki iş parçacığının
geri kalanı paralel ilerlerken yalnızca kritik bölgeye giriş anlarının sıraya girmesi
gerekiyor. Bu ders bekleyen bir tarafın kilit üzerinden nasıl ilerlediğini doğrudan ölçüyor.
Bu derste iki farklı kilit türü koşturuluyor: Mutex, tek bir tarafın aynı anda erişmesine
izin veren bir dışlama kilidi; RwLock, okuma ve yazma erişimini ayrı ayrı ele alan bir
okuma/yazma kilidi. İkisi de aynı temel soruna çözüm sunuyor — paylaşılan bir veriye
eşzamanlı erişimi güvenli kılmak — ama izin verdikleri paylaşımın derecesi farklı. Ders bu
farkı üç ayrı senaryoyla ölçüyor: kilidi tutan tarafın kendi başına ilerlemesi, kilidi
bekleyen tarafın başkası bitince ilerlemesi ve okuma/yazma kilidinin okuma tarafının bu
kuralı nasıl gevşettiği.
Kilidi Tutan Kendi Başına, Bekleyen Başkası Bitince
Bir dışlama kilidi kavramı burada yeniden kurulmuyor. İşletim Sistemi Kavramları kursu
kilidin ne için var olduğunu, JVM, Eşzamanlılık ve Başarım kursu bir kilidi tutan iş
parçacığının kimliğini ölçtü. Bu kurs farklı bir soru soruyor: kilidi tutan tarafın ve
kilidi bekleyen tarafın ilerlemek için neye ihtiyaç duyduğu. Yanıt kısa: kilit boşken
çağrılan bir lock kimseye ihtiyaç duymadan döner — kendi başına sınıfına girer. Kilit
doluyken çağrılan bir lock ise, kilidi tutan taraf onu bırakana kadar bekler — başkası
bitince sınıfına girer. Bu, Kanallarla Mesajlaşma dersindeki recv çağrısıyla aynı yapıya
sahip: ikisi de kendi dışında bir olayın gerçekleşmesini bekliyor, farkları beklenen olayın
bir gönderim mi yoksa bir serbest bırakma mı olması.
Kilidin kendisi, lock çağrısının döndürdüğü bir tutamaçla temsil ediliyor. Bu tutamaç
kapsam dışına çıktığında (ya da açıkça bırakıldığında) kilit otomatik olarak serbest kalıyor
— Sahiplik ve Bellek Modeli kursunun kurduğu bırakma kuralının bir uzantısı. Aşağıdaki
programda tutan iş parçacığının içindeki kayit adlı tutamaç, işlevin gövdesi bitip
kapsam dışına çıkana kadar kilidi elinde tutuyor; kilidin ne zaman bırakılacağı programcının
açık bir çağrısına değil, bu tutamacın ömrünün nerede bittiğine bağlı. Bu, önceki derste
görülen taşınan bir değerin sahiplik kuralıyla aynı düzenek — burada uygulanan yer kilit
tutamacı.
Bu ilişkiyi çalışma zamanının gerçek zamanlamasına bakmadan ölçmek gerekiyor, çünkü
zamanlama ortama bağlıdır ve bu kursta basılmıyor. Aşağıdaki program, kilidi tutan tarafın
kilide gerçekten dokunduğunu ve bekleyen tarafın gerçekten ondan sonra girdiğini bir kanal
el sıkışmasıyla kanıtlıyor: bekleyen taraf, tutan tarafın kilide girdiğine dair bir sinyal
almadan denemeye bile başlamıyor ve iki tarafın da paylaşılan bir deftere yazdığı kayıtların
sırası, kilidin kendisi tarafından zorlanıyor. Bu el sıkışma olmasaydı — yani bekleyen
kendi kilit çağrısını herhangi bir sinyal beklemeden yapsaydı — ölçüm hâlâ doğru sırayı
üretirdi, çünkü kilit zaten mutlak bir sıra garantisi veriyor; ama o durumda bekleyenin
çağrısının gerçekten A kilidi tutuyorken mi yoksa ondan önce mi yapıldığı belirsiz
kalırdı. El sıkışma, ölçümün hangi anı kanıtladığını netleştiriyor.
El Sıkışmayla Belirlenimci Sıra
Aşağıdaki program üç ayrı senaryoyu peş peşe koşturuyor ve üçü de aynı main işlevi
içinde, kendi kanallarıyla izole ediliyor. İlk senaryo tek bir bekleyeni ölçüyor ve iki ayrı
sinyal kanalı kullanıyor: a_icerde_gonder/a_icerde_al, tutanın kilide girdiğini
bekleyene bildiriyor; devam_gonder/devam_al ise bekleyenin denemeye başladığını
tutana bildiriyor. Bu iki yönlü el sıkışma, hangi tarafın kilide ne zaman dokunduğunu
kesinleştiriyor — kilidin kendisi (gunluk) bu sinyalleşmeden habersiz, yalnızca standart
lock çağrısıyla korunuyor.
// kilit.rs — kilidi tutan kendi basina ilerler, kilidi bekleyen baskasi bitince ilerler.
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
fn main() {
// --- disari cikma sirasi el sikismayla belirlenimci kilinir ---
let gunluk = Arc::new(Mutex::new(Vec::<String>::new()));
let (a_icerde_gonder, a_icerde_al) = channel::<()>();
let (devam_gonder, devam_al) = channel::<()>();
let g = Arc::clone(&gunluk);
let tutan = thread::spawn(move || {
// kendi basina: kilit bostu, kimseyi beklemeden aliniyor
let mut kayit = g.lock().unwrap();
kayit.push(String::from("A-girdi"));
a_icerde_gonder.send(()).unwrap();
devam_al.recv().unwrap(); // B'nin denemeye basladigindan emin olunana kadar tutuluyor
kayit.push(String::from("A-cikti"));
// kayit burada kapsam disina cikip birakiliyor
});
a_icerde_al.recv().unwrap();
let g = Arc::clone(&gunluk);
let bekleyen = thread::spawn(move || {
devam_gonder.send(()).unwrap();
// baskasi bitince: A kilidi birakana kadar burada bloklanir
let mut kayit = g.lock().unwrap();
kayit.push(String::from("B-girdi"));
kayit.push(String::from("B-cikti"));
});
tutan.join().unwrap();
bekleyen.join().unwrap();
println!("tek bekleyen sirasi: {:?}", *gunluk.lock().unwrap());
// --- iki bekleyen: kimin once girecegi olculmez, yalniz ikisinin de girdigi olculur ---
let gunluk2 = Arc::new(Mutex::new(Vec::<String>::new()));
{
let mut ilk = gunluk2.lock().unwrap();
ilk.push(String::from("baslangic"));
}
let g = Arc::clone(&gunluk2);
let c = thread::spawn(move || {
let mut kayit = g.lock().unwrap();
kayit.push(String::from("C-girdi"));
});
let g = Arc::clone(&gunluk2);
let d = thread::spawn(move || {
let mut kayit = g.lock().unwrap();
kayit.push(String::from("D-girdi"));
});
c.join().unwrap();
d.join().unwrap();
let mut girenler: Vec<String> = gunluk2
.lock()
.unwrap()
.iter()
.filter(|s| s.ends_with("-girdi"))
.cloned()
.collect();
girenler.sort();
println!("iki bekleyenin girdigi kayitlar (sira olculmedi): {:?}", girenler);
// --- okuma/yazma kilidi: birden fazla okuyucu ayni anda kilidi tutabiliyor ---
let deger = Arc::new(RwLock::new(42));
let (r1_icerde_gonder, r1_icerde_al) = channel::<()>();
let (r2_tamam_gonder, r2_tamam_al) = channel::<()>();
let (birak_gonder, birak_al) = channel::<()>();
let d1 = Arc::clone(°er);
let okuyucu1 = thread::spawn(move || {
let tutamac = d1.read().unwrap();
r1_icerde_gonder.send(()).unwrap();
birak_al.recv().unwrap();
*tutamac
});
r1_icerde_al.recv().unwrap();
let d2 = Arc::clone(°er);
let okuyucu2 = thread::spawn(move || {
// R1 hala okuma kilidini tutuyorken bu cagri da basariyla kilit aliyor
let tutamac = d2.read().unwrap();
let sonuc = *tutamac;
r2_tamam_gonder.send(()).unwrap();
sonuc
});
r2_tamam_al.recv().unwrap();
birak_gonder.send(()).unwrap();
let v1 = okuyucu1.join().unwrap();
let v2 = okuyucu2.join().unwrap();
println!();
println!("iki okuyucu ayni anda kilit tuttu, okunan degerler: {} ve {}", v1, v2);
println!("cok okuyana izin verildi: {}", v1 == v2);
}
tek bekleyen sirasi: ["A-girdi", "A-cikti", "B-girdi", "B-cikti"] iki bekleyenin girdigi kayitlar (sira olculmedi): ["C-girdi", "D-girdi"] iki okuyucu ayni anda kilit tuttu, okunan degerler: 42 ve 42 cok okuyana izin verildi: true
IP6. İlk satırdaki dörtlü sıra üç ayrı koşumda birebir aynı çıkıyor:
A-girdi, A-cikti, B-girdi, B-cikti. Bu sıra zamanlamadan değil, programın yapısından
geliyor. bekleyen iş parçacığı, tutan kilide girdiğine dair sinyali (a_icerde_al.recv())
almadan kendi kilit çağrısını hiç yapmıyor; tutan ise kendi kaydını (A-girdi) yazıp
sinyali gönderdikten sonra, bekleyenin denemeye başladığına dair bir onay
(devam_al.recv()) bekliyor ve ancak o onay geldikten sonra ikinci kaydını (A-cikti)
yazıp kilidi bırakıyor. Bu üç adımlı el sıkışma, bekleyenin kilit çağrısının gerçekten A
kilidi elinde tutarken yapıldığını garanti ediyor — bekleyen bu çağrıda bloklanıyor,
tutan kilidi bırakana kadar ilerleyemiyor. Bloklanma süresi ölçülmüyor, yalnızca sonucun
sırası: B girişleri hiçbir zaman A çıkışından önce görünmüyor.
İki Bekleyen: Sıra Ölçülmez
İkinci senaryoda C ve D aynı kilidi bekliyor. İkisi de sonunda giriyor — çıktı bunu
kanıtlıyor — ama hangisinin önce girdiği hiçbir yerde ölçülmüyor. Program bu belirsizliği
gizlemiyor, tersine yapısına gömüyor: kayıtlar toplanmadan önce sıralanıyor, tıpkı İş
Parçacığı Oluşturma dersindeki rapor kanalının yaptığı gibi. girenler listesi her koşumda
["C-girdi", "D-girdi"] olarak çıkıyor — bu, alfabetik sıralamanın sonucu, gerçek giriş
sırasının değil.
Bu senaryoda C ve D başlamadan önce günlüğe bir "baslangic" kaydı yazılıyor ve bu
kayıt raporlanan listede görünmüyor, çünkü girenler yalnızca -girdi ile biten satırları
süzüyor. Bu süzme bilinçli: gunluk2 içinde tuttuğu kayıtların tam sırası aslında bellidir
ve deterministiktir (baslangic her zaman ilk sırada), ama C-girdi ile D-girdi
arasındaki göreli sıra bu kesinliği taşımıyor. Süzme, ölçülebilecek olanla ölçülemeyecek
olanı aynı veri yapısı içinde ayırıyor.
Okuma/Yazma Kilidi: Çok Okuyana İzin
Üçüncü senaryo bir dışlama kilidiyle bir okuma/yazma kilidi arasındaki farkı doğrudan
gösteriyor. okuyucu1, okuma kilidini alıp elinde tutarken bir sinyal gönderiyor;
okuyucu2, bu sinyali aldıktan sonra aynı değere kendi okuma kilidini istiyor. Eğer bu
kilit bir dışlama kilidi olsaydı, okuyucu2 okuyucu1 kilidi bırakana kadar bloklanırdı —
ve okuyucu1, okuyucu2nin bir onay göndermesini beklediği için, bu onay hiçbir zaman
gelmez ve program kilitlenirdi. Program kilitlenmeden tamamlandığına göre, okuyucu2nin
okuma kilidi çağrısı gerçekten okuyucu1 hâlâ kilidi tutuyorken başarıyla dönmüş demektir.
Bu, ilk senaryodaki el sıkışmayla aynı mantığı tersine çeviriyor. Orada el sıkışma bir
bekleyişi kanıtlamak için kullanıldı: bekleyenin kilidi gerçekten tutan elindeyken
istediğini göstermek. Burada ise el sıkışma bir bekleyişin yokluğunu kanıtlamak için
kullanılıyor: eğer okuma çağrıları birbirini bloklasaydı, program bu el sıkışmayı hiçbir
zaman tamamlayamaz ve sonsuza kadar beklerdi. Programın normal biçimde bitmesi, kendi
başına okuma/yazma kilidinin okuma tarafının paylaşılabilir olduğunun kanıtı — bu derste
kullanılan ölçüm yöntemlerinden en dolaylısı, ama en kesini.
IP7. İki okuyucunun okuduğu değer birebir aynı (42 ve 42), çünkü ikisi de aynı
paylaşılan sayıyı okuyor ve okuma sırasında hiçbir yazma gerçekleşmiyor. Bu ölçüm, okuma/
yazma kilidinin okuma tarafının kendi içinde paylaşılabilir olduğunu gösteriyor: iki
okuyucu aynı anda kilide sahip olabiliyor, oysa aynı senaryo bir Mutex ile kurulsaydı
okuyucu2 kendi kilidini isteyemeden okuyucu1in onayını asla gönderemeyeceği için program
kilitlenirdi.
Bu farkın kaynağı, okuma/yazma kilidinin iki ayrı giriş noktası sunması. Bir dışlama kilidi tek bir çağrı taşır — kim çağırırsa çağırsın aynı kurala tabidir. Okuma/yazma kilidi ise okuma ve yazma için ayrı çağrılar taşır: okuma çağrısı, kilitte hiçbir yazıcı yokken ne kadar okuyucu isterse kabul eder; yazma çağrısı ise her zaman tek başına kalmayı ister, ne başka bir yazıcıyla ne de bir okuyucuyla paylaşmaz. Bu ölçümde yalnızca okuma tarafı koşturuldu; yazma çağrısının davranışı, dışlama kilidininkiyle özdeş — bir yazıcı kilide girdiğinde, o an kilitte bekleyen her taraf (ister okuyucu ister yazıcı) onun bitmesini bekler.
Sınırlayıcı Ölçüm
Kilit veriyi korur, sırayı kurmaz. Bir dışlama kilidi ya da bir okuma/yazma
kilidinin yazma tarafı, aynı anda yalnızca bir tarafın kritik bölgeye girmesini garanti
eder — bu derste ölçülen budur. Ama iki bekleyenden hangisinin önce gireceği, kilidin
kendi sözleşmesinin bir parçası değil; bu, işletim sisteminin zamanlayıcısına, kilidin iç
uygulamasına ve o anki yüke bağlı bir ayrıntıdır. C ve D örneğinde bu bilinçli olarak
ölçülmedi ve basılmadı — kayıtlar toplanmadan önce sıralanarak, hangi sıranın gerçekleştiği
sorusu programın çıktısından tamamen çıkarıldı.
Bu sınır, Muteks ve Okuma/Yazma Kilidi arasındaki farkı da netleştiriyor: okuma/yazma kilidinin okuma tarafı birden fazla bekleyene aynı anda giriş izni veriyor, ama bu izin de bir sıra garantisi taşımıyor — üçüncü bir okuyucu eklenmiş olsaydı, o okuyucunun ne zaman gireceği yine ölçülmezdi. Kilit çözdüğü tek sorun, paylaşılan veriye erişimin ne zaman güvenli olduğu; hangi tarafın önce erişeceği sorusunu hiç çözmüyor.
Bu ayrım rastgele bir seçim değil, kilidin verdiği garantinin doğal sınırı. Bir kilit, kaç tarafın kritik bölgede aynı anda bulunabileceğini (dışlama kilidinde bir, okuma/yazma kilidinin okuma tarafında sınırsız) tanımlıyor; kritik bölgeye giriş sırasının neye göre belirleneceğini tanımlamıyor, çünkü bu genellikle işletim sistemi zamanlayıcısının kendi politikasına bırakılan bir karar. Bu kursta hiçbir yerde bir zamanlayıcı politikasının adı geçmiyor ve geçmeyecek; ölçülen yalnızca kilidin verdiği ve vermediği garantiler.
Özet
- Kilidi tutan taraf kendi başına ilerler; kilidi bekleyen taraf, kilit bırakılana kadar başkası bitince ilerler. Bu ilişki bu derste bir kanal el sıkışmasıyla belirlenimci biçimde kanıtlandı.
- İki bekleyen taraf arasında hangisinin önce gireceği ölçülmez ve basılmaz; kayıtlar toplanmadan önce sıralanarak bu belirsizlik programın çıktısından çıkarılır.
- Okuma/yazma kilidinin okuma tarafı, aynı anda birden fazla okuyucuya izin verir — bu, aynı senaryonun bir dışlama kilidiyle kilitlenmeye yol açacağı bir durumla karşılaştırıldı.
- Kilit paylaşılan veriye erişimi korur, ama hangi tarafın önce gireceğine dair hiçbir garanti vermez; bu iki şey birbirinden bağımsızdır.
- JVM, Eşzamanlılık ve Başarım kursu bir kilidi hangi iş parçacığının tuttuğunu ölçtü, bu kurs aynı kilidi bekleyen tarafın ilerlemek için neye ihtiyaç duyduğunu ölçüyor.
Sonraki Adım
Kilit bekleyen tarafı durduruyor, çünkü paylaşılan veriye erişim sırayla verilmek zorunda.
Bu derste görülen üç senaryonun hepsinde bir tarafın kilit için beklemesi gerekti — ya kilit
tutulduğu için (bekleyen, C, D), ya da bir yazma isteği söz konusu olsaydı (okuma/
yazma kilidinin yazma tarafı). Ama bazı paylaşımlar bu kadar sıkı bir korumaya ihtiyaç
duymuyor — özellikle paylaşılan değer tek bir sayı gibi basitse ve üzerinde yapılan işlem
donanımın kendisinin tek adımda gerçekleştirebileceği kadar küçükse. Sıradaki ders, hiç
kimsenin beklemediği bir paylaşım biçimini ölçüyor: bir sayacın kilitsiz, atomik işlemlerle,
kimseye ihtiyaç duymadan ilerlemesi — ve bunun kilitli bir sayaçla aynı sonucu verip
vermediği.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.