İçeriğe geç
academia.sh

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

  • Send bir tipin sahipliğinin başka bir iş parçacığına taşınabildiğini, Sync bir 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/Sync olan bir yapı, Arc<Mutex<...>> içine konduğunda sınırı geçebiliyor; ölçümde birinci kendi başına ilerledi, ikinci birincinin 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ç) ve Mutex (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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat