İçeriğe geç
academia.sh

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(&deger);
    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(&deger);
    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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat