İçeriğe geç
academia.sh

Ders 02 / 10

Kanallarla Mesajlaşma

Bir kanaldan haber bekleyen taraf başkası bitince ilerler, haberi kuran ve gönderen taraf kendi başına ilerler; gönderilen değerin sahipliği kanala taşınır ve gönderen tarafta bir daha kullanılamaz.

Yayın Güncelleme

Bu derste

  • Bir kanaldan gönderilen değerin sahipliğinin alıcıya taşındığını ve gönderende kalmadığını gösterir
  • Kanaldan haber bekleyen tarafın başkası bitince, haberi kuran tarafın kendi başına ilerlediğini ölçer
  • Beklenen sayıda kayıt gelmediğinde eksikliğin nasıl ölçüldüğünü gösterir
İçindekiler

Önceki ders bir sinyal kanalını yalnızca bir uyandırma aracı olarak kullandı: gönderilen değer boştu, önemli olan yalnızca gönderimin gerçekleşip gerçekleşmediğiydi. Bu ders aynı düzeneği bir yükle çalıştırıyor. Kanaldan geçen değer artık boş değil; bir veri taşıyor ve o verinin kime ait olduğu sorusu, ilerleme sorusuyla aynı derste birlikte ölçülüyor.

Kanal kavramının kendisi bu kursta kurulmuyor. Go’nun Eşzamanlılık kursu kanalı ayrıntılı biçimde ele aldı, ama orada ölçülen kanalın bir durma düzeneği olarak kullanılmasıydı — bir goroutine ne zaman ve neden durur sorusunun bir parçasıydı. Bu derste aynı düzenek bir ilerleme aracı olarak ölçülüyor: kanal burada bir birimi bitirmiyor, bir birimi başlatıyor ya da devam ettiriyor. Kanalın kendi işleyişi — tamponlu ya da tamponsuz olması, kapanma davranışı, çok üretici tek tüketici düzeni — o kursta kapandı ve burada tekrarlanmıyor; bu ders yalnızca kanalın ilerleme sınıflarına nasıl karşılık geldiğini soruyor.

Sahiplik Taşıyan Kanal

Sahiplik ve Bellek Modeli kursu bir değerin taşınmasının ne anlama geldiğini kurdu: bir değer bir bağlamadan diğerine taşındığında, ilk bağlama artık geçerli değildir ve değere yalnızca yeni bağlama üzerinden erişilebilir. Bir kanaldan gönderim de bu kuralın bir örneğidir — send çağrısı değeri alır ve kanala taşır; gönderen taraf o değere bir daha dokunamaz.

Bunun sonucu, alıcı tarafın değeri kilitsiz biçimde değiştirebilmesidir. Aşağıdaki program bir listeyi bir iş parçacığından diğerine gönderiyor; alan taraf listeyi büyütüyor, ama bunu yaparken hiçbir kilide ihtiyaç duymuyor, çünkü listenin o andaki tek sahibi kendisi.

Bu, iki iş parçacığının aynı öbek belleğine erişebildiği bir durumla karıştırılmamalı. liste değeri veren iş parçacığında kurulduğunda öbekte bir yer kaplıyor; send çağrısı bu yerin adresini değil, o yerin sahipliğini taşıyor. Alan taraf liste.push çağırdığında büyüme gerekiyorsa Rust’ın büyüyen dizi tipi öbekte yeni bir yer ayırıp eski içeriği oraya kopyalıyor — ama bu işlem sırasında ikinci bir tarafın aynı belleğe erişme ihtimali hiç yok, çünkü veren iş parçacığı zaten birleştirilmiş ve elinde liste adlı bir bağlama kalmamış durumda.

// tasima.rs — kanaldan gelen deger tasindigi icin kilitsiz mutasyona aciktir.

use std::sync::mpsc::channel;
use std::thread;

fn main() {
    let (gonder, al) = channel::<Vec<i32>>();

    let veren = thread::spawn(move || {
        let liste = vec![1, 2, 3];
        gonder.send(liste).unwrap();
        // liste burada artik kullanilamaz: sahiplik kanala tasindi.
    });
    veren.join().unwrap();

    // --- alan taraf: kilit gerekmeden dogrudan buyutuyor, cunku tek sahibi kendisi ---
    let mut liste = al.recv().unwrap();
    liste.push(4);
    liste.push(5);

    println!("alinan listenin uzunlugu: {}", liste.len());
    println!("kilitsiz buyutuldu, cunku baska hicbir yerde bu deger paylasilmiyor");
}
alinan listenin uzunlugu: 5
kilitsiz buyutuldu, cunku baska hicbir yerde bu deger paylasilmiyor

IP2. liste değişkeni send çağrısına verildikten sonra kaynakta bir daha görünmüyor; görünseydi kaynak derlenmezdi, çünkü taşınmış bir değere erişim Rust’ın sahiplik kuralının ihlalidir. Bu kısıt bu derste bir hata olarak koşturulmuyor — yalnızca düzyazıda tarif ediliyor — ve yanına, kısıta uyan kabul edilmiş tasarım konuyor: veren taraf değeri kurup gönderiyor, alan taraf devralıp büyütüyor, ikisi arasında hiçbir an ortak bir referans bulunmuyor. Kilidi Muteks ve Okuma/Yazma Kilidi dersi anlatacak; oradaki paylaşım burada yoktur, çünkü paylaşılan bir şey değil, tamamen el değiştiren bir şey var.

Bu ayrım kanalın kendi türüne de yansıyor: channel::<Vec<i32>>() yazımı, kanalın yalnızca Vec<i32> tipinde değerler taşıyacağını, bu değerlerin bir referans ya da bir kopya değil, tam ve tek bir sahiplik olarak geçeceğini söylüyor. Alan taraf al.recv() çağrısından dönen değeri aldığında, o değerin kaynaktaki tek nüshası artık kendi elindedir; liste.push çağrısının hiçbir eşzamanlılık ilkeline ihtiyaç duymaması bu yüzdendir — büyütülen liste, büyütüldüğü anda başka hiçbir bağlamanın erişebileceği bir yerde durmuyor. Bu, kanalın neden bir dışlama kilidinden farklı bir araç olduğunu da açıklıyor: kilit paylaşılan bir değere sırayla erişim sağlar, kanal ise değeri hiç paylaşmadan bütünüyle devrediyor.

Veren Kendi Başına, Bekleyen Başkası Bitince

Kanaldan geçen değerin sahiplik hikâyesi bir yana, aynı gönderim aynı zamanda bir ilerleme sorusu taşıyor. Aşağıdaki ölçüm bunu iki tarafla ayrı ayrı gösteriyor: veren taraf üç ayrı ileti kurup gönderiyor, hiçbir şeyi beklemeden; bekleyen taraf her ileti için ayrı bir alım çağrısı yapıyor ve bu çağrı, karşı taraf göndermeden dönmüyor.

Bu çift, önceki dersin ölçüm çekirdeğindeki iplik-isaret-veren ile iplik-kanal-bekleyen satırlarının aynısı — orada sinyal boş bir değerdi (()), burada aynı ilişki gerçek bir yük taşıyan üç ayrı iletiyle tekrarlanıyor. Sınıflandırma değişmiyor: sinyali kuran taraf orada da burada da kendi başına, sinyali bekleyen taraf orada da burada da başkası bitince ilerliyor. Değişen, kanaldan geçenin ne taşıdığı.

Ölçümde iki ayrı kanal birlikte çalışıyor. Birincisi ileti_gonder/ileti_al çifti: asıl yük olan Ileti değerlerini taşıyor, her biri bir içerik dizgesi ve bir sıra numarası taşıyan bir yapı. İkincisi, önceki dersten tanıdık olan rapor kanalı: her iki tarafın da kendi ilerlemesini bir deftere yazması için kullanılıyor ve bu defter, sırası ölçülmeden önce sıralanıyor. İki kanalın ayrılması bilinçli — asıl mesajlaşmayı ölçen kanal ile o ölçümü doğrulamak için kullanılan rapor kanalı karışırsa, hangi kaydın gerçek işin parçası hangisinin yalnızca gözlem olduğu belirsizleşir.

Kodda hem rapor_gonder.clone() hem Ileti değerinin taşınması bir arada görünüyor ve ikisi farklı şeyler. Kanalın gönderen ucu klonlanabilir bir tiptir: rapor_gonder.clone() çağrısı, aynı kanala yazabilen ikinci bir tutamaç üretir; iki tutamaç veri paylaşmaz, yalnızca aynı hedefe erişim hakkını paylaşır. Her iş parçacığının kendi tutamacına sahip olması, birden fazla göndericinin aynı kanala eşzamanlı yazabilmesini sağlıyor — bu, send() çağrısının kendisinin dışlama kilidi gerektirmemesinin nedeni. Ileti değeri ise hiç klonlanmıyor: send çağrısına doğrudan taşınıyor, çünkü onun tam olarak tek bir sahibi olması gerekiyor. Bir tutamacın çoğaltılabilir olması ile bir değerin tek sahipli kalması, aynı programda yan yana duran iki ayrı kural.

Ölçümdeki Ileti yapısı iki alan taşıyor: icerik bir dizge, sira bir tam sayı. İkisinin birlikte gönderilmesi bilinçli bir seçim — kanal yalnızca tek bir tamsayı ya da tek bir dizge değil, keyfi karmaşıklıkta bir değeri de aynı sahiplik kuralıyla taşıyabildiğini gösteriyor. sira alanı ayrıca ölçümün kendisine hizmet ediyor: bekleyen tarafın topladığı toplam_sira, üç iletinin de eksiksiz ve doğru içerikle geldiğini, yalnızca sayılarının değil değerlerinin de doğru taşındığını kanıtlıyor.

// kanal.rs — kanaldan haber bekleyen baskasi bitince ilerler, haberi veren kendi basina ilerler.

use std::sync::mpsc::channel;
use std::thread;

struct Ileti {
    icerik: String,
    sira: u32,
}

fn main() {
    let (rapor_gonder, rapor_al) = channel::<String>();

    // --- veren taraf: kendi basina, kimseyi beklemeden ileti kuruyor ve gonderiyor ---
    let (ileti_gonder, ileti_al) = channel::<Ileti>();
    let rg = rapor_gonder.clone();
    let veren = thread::spawn(move || {
        for sira in 1..=3 {
            let deger = Ileti {
                icerik: format!("kayit-{}", sira),
                sira,
            };
            ileti_gonder.send(deger).unwrap();
            rg.send(format!("verici-gonderdi-{}", sira)).unwrap();
        }
        // deger burada artik yok: sahiplik gonder cagrisiyla kanala tasindi.
    });

    // --- bekleyen taraf: baskasi bitince ilerliyor, her ileti icin ayri recv() ---
    let rg = rapor_gonder.clone();
    let bekleyen = thread::spawn(move || {
        let mut toplam_sira = 0u32;
        for _ in 1..=3 {
            let ileti = ileti_al.recv().unwrap();
            toplam_sira += ileti.sira;
            rg.send(format!("bekleyen-aldi-{}", ileti.icerik)).unwrap();
        }
        toplam_sira
    });

    veren.join().unwrap();
    let toplam = bekleyen.join().unwrap();

    let mut kayitlar: Vec<String> = Vec::new();
    for _ in 0..6 {
        kayitlar.push(rapor_al.recv().unwrap());
    }
    kayitlar.sort();

    println!("tam senaryo — toplanan sira: {}", toplam);
    println!("kayitlar ({}):", kayitlar.len());
    for k in &kayitlar {
        println!("  {}", k);
    }

    // --- eksik senaryo: beklenen 3 verici var ama yalniz 2'si baslatildi ---
    let (rapor_gonder2, rapor_al2) = channel::<String>();
    let (ileti_gonder2, ileti_al2) = channel::<Ileti>();
    let rg2 = rapor_gonder2.clone();
    let eksik_veren = thread::spawn(move || {
        for sira in 1..=2 {
            let deger = Ileti {
                icerik: format!("eksik-kayit-{}", sira),
                sira,
            };
            ileti_gonder2.send(deger).unwrap();
            rg2.send(format!("eksik-verici-gonderdi-{}", sira)).unwrap();
        }
    });
    eksik_veren.join().unwrap();
    drop(rapor_gonder2);

    let mut eksik_kayitlar: Vec<String> = Vec::new();
    while let Ok(k) = rapor_al2.recv() {
        eksik_kayitlar.push(k);
    }
    eksik_kayitlar.sort();

    let mut alinan_iletiler: Vec<String> = Vec::new();
    while let Ok(ileti) = ileti_al2.try_recv() {
        alinan_iletiler.push(ileti.icerik);
    }
    alinan_iletiler.sort();

    let beklenen = 3;
    println!();
    println!("eksik senaryo — beklenen: {}, gelen: {}", beklenen, alinan_iletiler.len());
    for k in &alinan_iletiler {
        println!("  {}", k);
    }
    println!("eksik kayit sayisi: {}", beklenen - alinan_iletiler.len());
}
tam senaryo — toplanan sira: 6
kayitlar (6):
  bekleyen-aldi-kayit-1
  bekleyen-aldi-kayit-2
  bekleyen-aldi-kayit-3
  verici-gonderdi-1
  verici-gonderdi-2
  verici-gonderdi-3

eksik senaryo — beklenen: 3, gelen: 2
  eksik-kayit-1
  eksik-kayit-2
eksik kayit sayisi: 1

IP3. Tam senaryoda veren iş parçacığı üç ileti kurup gönderiyor ve hiçbir kaydı başkasının bitmesini beklemeden yazıyor — kendi başına sınıfına giriyor. bekleyen iş parçacığı ise her recv çağrısında karşı tarafın bir sonraki göndermesini bekliyor — başkası bitince sınıfına giriyor. Toplanan sıra 1 + 2 + 3 = 6; bu değer, üç iletinin de sırasıyla ve eksiksiz alındığının doğrulamasıdır ve üç ayrı koşumda birebir aynı çıkıyor.

toplam_sira değerinin kendisi de önceki dersin bir kuralını sürdürüyor: bekleyen iş parçacığının kapanışı bu sayıyı döndürüyor ve bekleyen.join() çağrısı bu döndürülen değeri çağıran tarafa taşıyor. İş Parçacığı Oluşturma dersinde kurulan kural burada da geçerli — kapanışın ürettiği değer, sahiplik ve Bellek Modeli kursunun taşıma kuralıyla birleştirme çağrısına geçiyor ve çağıran taraf o değeri birleştirme bitmeden okuyamıyor. Burada fark edilecek olan, bu taşımanın hem Ileti yapıları için hem de basit bir u32 toplamı için aynı yolu izlemesi: kanal ve birleştirme, farklı veri biçimlerinde aynı sahiplik kuralını uyguluyor.

Sınırlayıcı Ölçüm

Bu derste ölçülen ilerleme yalnızca iki uçlu: ya bir ileti gelir ve bekleyen taraf devam eder, ya da hiç gelmez ve bekleyen taraf orada kalır. Kaç iletinin, hangi hızda geldiği bu kursta hiç ölçülmüyor — yalnızca üç iletinin de gelip gelmediği. Bekleyen taraf süresiz bekler. recv çağrısı, karşı taraftan bir ileti gelene kadar döner. Eğer beklenen sayıda gönderim hiç gerçekleşmezse, kayıt hiç oluşmaz — ve gerçek bir programda bekleyen taraf sonsuza kadar bloke kalırdı. Bu dersin ikinci ölçümü, bu durumu bir kilitlenmeye girmeden göstermek için beklenen ve gerçekleşen sayıyı ayrı ayrı hesaplıyor: “eksik senaryo”da üç ileti bekleniyordu, ama veren taraf yalnızca iki tanesini gönderip bitirdi. Program üçüncü iletiyi sonsuza kadar beklemek yerine, göndericinin ve rapor kanalının kapandığını (drop(rapor_gonder2)) algılıyor, gelen kayıtları topluyor ve eksik sayıyı 1 olarak raporluyor.

Bu algılama şansa bırakılmıyor: drop(rapor_gonder2) çağrısı, kanalın gönderen ucunun tek kalan kopyasını açıkça bırakıyor. Sahiplik ve Bellek Modeli kursu bir değerin bırakılmasının ömrünün bittiği noktalardan biri olduğunu kurdu; burada aynı düzenek bir kanalın kapanma sinyaline dönüşüyor. Rust’ın kanal uygulaması, gönderen uçların hepsi bırakıldığında alıcı tarafın bir sonraki recv çağrısını hata olarak sonlandırır — programda kullanılan while let Ok(k) = ... yazımı bu hatayı örtük biçimde döngü sonu sayıyor. Eğer rapor_gonder2 bırakılmasaydı, döngü kanalın hiçbir zaman kapanmayacağını varsayar ve sonsuza kadar beklerdi; bırakma burada yalnızca bir bellek temizliği değil, alıcıya “artık gelecek başka bir şey yok” diyen bir sinyal.

Bu, gerçek bir uygulamanın kendi süresizce bekleme riskini nasıl yönettiğinin bir örneği değil — o, İptal ve Zaman Aşımı dersinin konusu olan zaman aşımı düzeneğine yakın bir soru ve orada ayrı ele alınacak. Burada ölçülen yalnızca şu: beklenen sayı ile gelen sayı arasındaki fark, hangi kaydın eksik olduğunu (bu örnekte üçüncü ileti) doğrudan gösteriyor, üçüncü iletinin ne zaman geleceğini değil — çünkü hiç gelmedi.

Eksik senaryonun kalan iletileri toplarken kullanılan çağrı da bu sınırı gösteriyor. Program eksik_veren iş parçacığını birleştirdikten sonra iletileri topluyor; bu noktada gönderilecek her şey zaten gönderilmiş durumda, bu yüzden bekleyen bir çağrıya gerek yok — kanalda ne varsa hemen okunup toplanabiliyor. Bu sıralama belirlenimciliği koruyor: iletiler her koşumda aynı sırayla, aynı sayıda toplanıyor, çünkü toplama işlemi hiçbir canlı göndericinin daha ileti gönderip göndermeyeceğine bağlı değil. Beklenen sayının üçüncü iletisi bu yüzden hiçbir zaman gelmiyor ve program onu beklemeye çalışmıyor; yalnızca beklenen ile gelen sayıyı karşılaştırıp farkı yazdırıyor.

Özet

  • Kanaldan gönderilen bir değerin sahipliği alıcıya taşınır; gönderen taraf o değere bir daha erişemez ve alan taraf hiçbir kilide ihtiyaç duymadan değeri değiştirebilir.
  • Haberi kuran ve gönderen taraf kendi başına ilerler; kanaldan haber bekleyen taraf başkası bitince ilerler — bu ayrım her recv çağrısı için ayrı ayrı geçerlidir.
  • Tam senaryoda üç iletinin toplam sırası (6) üç ayrı koşumda birebir aynı çıkar; bu, gönderim ile alımın eksiksiz eşleştiğinin ölçüsüdür.
  • Beklenen sayıda gönderim gerçekleşmezse bekleyen taraf süresiz bekler; eksik kayıt, beklenen ile gelen sayı arasındaki farkla ölçülür, asla bir bekleme süresiyle değil.
  • Sınır çizgisi Muteks ve Okuma/Yazma Kilidi dersine bırakılıyor: burada paylaşılan bir değer yok, yalnızca el değiştiren bir değer var.

Sonraki Adım

Bu derste bir değerin bir iş parçacığından diğerine geçebilmesi iki örnekle gösterildi: Vec<i32> ve Ileti. İkisi de sorunsuzca geçti, ama bu geçiş şimdiye kadar hiç sorgulanmadı — sanki her tip bir kanaldan geçebilirmiş gibi davranıldı. Aslında bu izin tipin kendisinde yazılı bir koşuldur ve her tip bu koşulu sağlamaz. Sıradaki ders bu koşulun nerede tutulduğunu soruyor: hangi tipler bir iş parçacığı sınırını geçebilir, hangileri geçemez ve bu, derleme zamanında nasıl bilinir.

İ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