İçeriğe geç
academia.sh

Ders 05 / 09

Eşzamanlılık Modeli

Aynı kodun aynı girdilerle farklı sayı vermesi bir sıra sorunudur. Ders okuma ile yazma arasına giren geçişin bir sayacı kaybettirdiğini, dışlama kilidinin sonucu koddan çıkardığını ve Ractor'ın paylaşılan durumu kaldırdığını ölçer.

Yayın Güncelleme

Bu derste

  • Kilitsiz bir sayaçta sonucun iş parçacıklarının sırasına bağlı olduğunu ölçer
  • Küresel yorumlayıcı kilidinin okuma ile yazma arasını korumadığını açıklar
  • Dışlama kilidinin ve kuyruğun sonucu sıradan bağımsız kıldığını gösterir
  • Ractor'ın paylaşılan durumu kaldırdığını, ama dondurmanın yüzeysel kaldığını ölçer
İçindekiler

Önceki ders saati okuyan bir testin yarın düşebileceğini gösterdi: o sonucu ortam belirliyordu. Bu ders sıraya döner. Dört iş parçacığı bir sayacı bin kez artırdığında dört bin beklenir; sayaç bazen daha azını verir ve kod hiç değişmemiştir. Kursun ölçü ekseni sürer: bu sonucu ne belirliyor? Yanıt dört addan biridir, çıktıda ASCII yazılır: kod, bagimlilik, sira, ortam. Bu dersin gözlemi kilitsiz sayac satırıdır ve belirleyeni sıradır.

M08/K04’te bir adımın hangi iş parçacığına düştüğü sorulmuştu. Burada iş parçacığının kimliği değil, sıranın sonucu değiştirip değiştirmediği sorulur. Çekirdeğin belirleyen.rb dosyası aşağıda olduğu gibi verilir; blok kaynaktır, çıktısı yoktur ve require_relative ile çekilir. Çekirdekteki kilit kilit dosyasının içeriğidir, yani kurulan sürümlerdir. Dışlama kilidi bu adla yalnız adı paylaşır; bu yüzden dersin dışlama kilidi değişkeni dislama olarak adlandırılır.

# belirleyen.rb — bir kosum modeli ve bir sonucu neyin belirledigini siniflandiran yardimci

# Kosum: sonucun bagli olabilecegi uc girdi. Bu uc girdinin disinda kalan her sey koddur.
Kosum = Data.define(:kilit, :tohum, :ortam)

TABAN = Kosum.new(
  kilit: { "bicimleyici" => "1.4.2", "ayristirici" => "2.0.1" },
  tohum: 1,
  ortam: { saat: Time.utc(2026, 10, 16, 9, 0), cekirdek: 4, degiskenler: {} }
)

# Her degisken tek bir girdiyi degistirir; otekiler tabandaki gibi kalir.
DEGISKENLER = {
  bagimlilik: TABAN.with(kilit: { "bicimleyici" => "1.5.0", "ayristirici" => "2.1.0" }),
  sira: TABAN.with(tohum: 2),
  ortam: TABAN.with(ortam: { saat: Time.utc(2027, 3, 1, 9, 0), cekirdek: 8,
                             degiskenler: { "ISCI_SAYISI" => "3" } })
}

# Kural: hesap once tabanda, sonra her degiskende bir kez kosar. Sonucu degistiren ilk
# degisken (bagimlilik, sira, ortam sirasiyla) belirleyendir; hicbiri degistirmiyorsa kod.
def belirleyen(hesap)
  taban = hesap.call(TABAN)
  degisti = DEGISKENLER.to_h { |ad, kosum| [ad, hesap.call(kosum) != taban] }
  tur = degisti.key(true)&.to_s || "kod"
  kanit = degisti.map { |ad, d| "#{ad[0].upcase}#{d ? '+' : '-'}" }.join(" ")
  [tur, kanit, taban]
end

Okuma ile Yazma Arasındaki Boşluk

Çekirdeğin sayacı gerçek iş parçacığı değil, bir zamanlayıcı modelidir. İki iş parçacığı sayacı okur, bir fazlasını yazar ve her biri üç artış yapar. Okuma ile yazma arasında geçiş olabilir; sıradaki adımı hangi iş parçacığının atacağını tohum seçer. Doğru sonuç altıdır. Model gerçek iş parçacıklarının yerine konmuştur, çünkü gerçek iş parçacıklarının sırası koşumdan koşuma değişebilir ve basılan sayı bir daha aynı çıkmaz. Tohum sırayı sabitler; böylece bozuk sonuç her koşumda aynı çıkar ve ölçülebilir olur.

# sayac.rb — zamanlayici modeli: kilitsiz sayac
# Zamanlayici modeli: iki is parcacigi sayaci okur ve bir fazlasini yazar; okuma ile yazma
# arasinda gecis olabilir. Siradaki adimi hangi is parcacigin atacagini tohum secer.
def kilitsiz_sayac(kosum, artis: 3)
  rastgele = Random.new(kosum.tohum)
  sayac = 0
  isler = Array.new(2) { { kalan: artis, okunan: nil } }
  loop do
    bekleyen = isler.select { |i| i[:kalan].positive? }
    break if bekleyen.empty?

    is = bekleyen.sample(random: rastgele)
    if is[:okunan].nil?
      is[:okunan] = sayac
    else
      sayac = is[:okunan] + 1
      is[:okunan] = nil
      is[:kalan] -= 1
    end
  end
  sayac
end
# siralar.rb — kilitsiz sayac: belirleyen ve alti tohum
require_relative "belirleyen"
require_relative "sayac"

p belirleyen(->(k) { kilitsiz_sayac(k) })
sonuclar = (1..6).map { |tohum| kilitsiz_sayac(TABAN.with(tohum: tohum)) }
p sonuclar
["sira", "B- S+ O-", 6]
[6, 3, 5, 4, 4, 4]

İlk satır çekirdeğin sonucudur: tabanda sayaç altıdır ve kanıtta yalnız S+ vardır. Kilit dosyasını ya da ortamı değiştirmek sonucu oynatmaz; tohumu değiştirmek oynatır. İkinci satır altı tohumun sonuçlarını gösterir: dört ayrı değer çıkar ve yalnız birinde sayaç doğru olan altıyı verir. Kayıp artış, iki iş parçacığının aynı değeri okuyup aynı değeri yazmasından gelir. RB1. Bu sonuçlar yalnız iki iş parçacığı ve üçer artış içindir; daha çok iş parçacığında kayıp oranı bu ölçümden çıkmaz.

Küresel Yorumlayıcı Kilidi Neyi Korumaz

Ruby’nin küresel yorumlayıcı kilidi (global interpreter lock), aynı anda yalnız bir iş parçacığının Ruby kodu koşturmasını sağlar. Bu, iki iş parçacığının aynı bellek konumuna aynı anda yazmasını engeller; ama bir iş parçacığının okumasından yazmasına kadar geçen aralığı korumaz. sayac += 1 tek bir adım gibi görünür, oysa okuma, toplama ve yazmadır. Zamanlayıcı bu üç adımın arasında başka bir iş parçacığına geçebilir ve modelin yaptığı tam olarak budur. Bu yüzden gerçek iş parçacıklarıyla yazılmış kilitsiz bir sayaç bu derste basılmaz: sonucu koşumdan koşuma değişebilir.

Sıranın sonucu değiştirmediği tasarım, aradaki boşluğu kapatır. Dışlama kilidi (mutex), bir bölgeye aynı anda yalnız bir iş parçacığını alır. Aşağıdaki blok dört gerçek iş parçacığı kurar, her biri sayacı bin kez artırır ve her artış dislama kilidinin içinde yapılır.

# dislama.rb — dort gercek is parcacigi, dislama kilidiyle
require_relative "belirleyen"

def dislamali_sayac(is_parcacigi: 4, artis: 1000)
  sayac = 0
  dislama = Mutex.new
  isler = Array.new(is_parcacigi) do
    Thread.new do
      artis.times { dislama.synchronize { sayac += 1 } }
    end
  end
  isler.each(&:join)
  sayac
end

p dislamali_sayac
p belirleyen(->(_k) { dislamali_sayac })
4000
["kod", "B- S- O-", 4000]

Sonuç dört bindir. Kural bu hesabı üç değişkende de koşturdu ve sonuç hiçbirinde değişmedi; kanıt B- S- O-, belirleyen koddur. Kilitsiz model sıraya bağlıydı, dışlama kilidiyle yazılmış gerçek sayaç ise kendi koduna bağlıdır. RB2. Bu kuralın üç değişkeni iş parçacığı sırasını doğrudan değiştirmez: tohum yalnız modeldeki zamanlayıcıyı sürer. Dört bin, dışlama kilidinin sırayı değiştirdiğinde bile sonucu koruduğunu kanıtlamaz; yalnız bu koşumda tutarlı olduğunu söyler.

Kuyruk ve İş Parçacığındaki İstisna

Paylaşılan durumu kilitlemek tek yol değildir. Üretici ile tüketici bir kuyruk (queue) üzerinden konuşursa değer bir elden ötekine geçer ve iki taraf aynı değişkene yazmaz. Aşağıdaki blokta üretici sıfırdan dörde kadar sayıları kuyruğa bırakır, tüketici toplar.

# kuyruk.rb — uretici ve tuketici bir kuyruktan konusur
kuyruk = Thread::Queue.new
uretici = Thread.new do
  5.times { |i| kuyruk << i }
  kuyruk << :bitti
end
toplam = 0
tuketici = Thread.new do
  while (oge = kuyruk.pop) != :bitti
    toplam += oge
  end
end
[uretici, tuketici].each(&:join)
p toplam
10

Toplam on çıktı. toplam değişkenini yalnız tüketici yazar, bu yüzden okuma ile yazma arasına giren başka bir yazan yoktur. Kuyruk sırayı korur ama sonuç sıradan da bağımsızdır: toplama sırası sonucu değiştirmez.

Bir iş parçacığında patlayan istisna sessizce kaybolmaz; ana iş parçacığında join çağrısında yeniden yükselir. Standart hataya iz düşmesin diye blok önce raporlamayı kapatır. İstisna iletisi dersin kendi raise çağrısındadır.

# istisna.rb — is parcacigindaki istisna join'de ana is parcacigina doner
Thread.report_on_exception = false
is = Thread.new { raise "patladi" }
begin
  is.join
rescue => e
  puts "yakalandi: #{e.class} #{e.message}"
end
p is.status
yakalandi: RuntimeError patladi
nil

İstisna RuntimeError olarak join’de yakalandı. İş parçacığının durumu nil’dir: iş parçacığı bir istisnayla bitti. join çağrılmasaydı istisna ana iş parçacığına hiç ulaşmazdı. RB3. Bu ölçüm tek bir iş parçacığı ve tek bir istisna içindir; birden çok iş parçacığının aynı anda patlaması burada ölçülmedi.

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

Dışlama kilidi paylaşılan durumu sıraya karşı korur. Ractor başka bir yol açar: bir Ractor, yalıtılmış bellekle koşan yürütme birimidir ve dışarıdaki yerel değişkenlere erişemez. Paylaşılan durum baştan yoktur, bu yüzden korunacak bir şey de kalmaz. İlk blok bir Ractor’ın değer döndürdüğünü, ikincisi dışarıdaki bir değişkene dokunan bloğun reddedildiğini ölçer.

# ractor.rb — yalitilmis bellekle kosan yurutme birimi
p Ractor.new { (1..10).sum }.value

dis = [1, 2, 3]
begin
  Ractor.new { dis.size }
rescue ArgumentError => e
  puts e.class
end
55
ArgumentError

İlk Ractor elli beş verdi. İkincisi ArgumentError ile reddedildi: blok dışarıdaki dis değişkenini okuyordu. Yalıtım sırayı değil, paylaşılan durumu kaldırır. Sıra hâlâ belirsizdir; ama iki Ractor aynı değişkene yazamayacağı için sıranın sonucu bozacağı yer kalmaz. Bir nesnenin Ractor’lar arasında paylaşılabilmesi için paylaşılabilir nesne olması gerekir. Dondurmak bunun için yetmez.

# paylasilabilir.rb — dondurmak yuzeyseldir
sade = [1, 2].freeze
ic_degisken = [+"a"].freeze
p Ractor.shareable?(sade)
p Ractor.shareable?(ic_degisken)
p Ractor.shareable?(Ractor.make_shareable([+"a"]))
true
false
true

Donmuş ve içi sayılardan oluşan dizi paylaşılabilirdir. İçinde donmamış bir dize taşıyan donmuş dizi değildir: dondurma yalnız dizinin kendisini dondurur, içindekileri değil. make_shareable içindekileri de dondurur ve sonuç paylaşılabilir olur. RB4. Bu üç sonuç Ruby 4.0.7’de ölçüldü; Ractor’ın kuralları sürümler arasında değişebilir.

Sınırlayıcı sonuç şudur: Ractor, kuralın sira sınıfını yok etmez. Kilitsiz model sıraya bağlıydı, çünkü iki iş parçacığı bir değişkeni paylaşıyordu. Paylaşım kalkarsa sıra sonucu belirlemez; paylaşım kalkmadan sıra belirleyendir.

Özet

  • Kilitsiz sayacı sıra belirler: aynı kod, aynı ortam ve aynı kilit dosyasıyla altı tohum dört farklı sonuç verdi, yalnız biri doğruydu.
  • Küresel yorumlayıcı kilidi okuma ile yazma arasını korumaz; sayac += 1 üç adımdır.
  • Dışlama kilidi sayacı dört bine sabitledi ve kural sınıfı kod verdi; sıra sonucu artık belirlemiyor.
  • Kuyruk değeri bir elden ötekine geçirir; iş parçacığındaki istisna join çağrısında yeniden yükselir.
  • Ractor paylaşılan durumu kaldırır; dondurma yüzeyseldir ve içi donmamış bir dizi paylaşılabilir sayılmaz.

Sonraki Adım

Kilitsiz sayacın neden kimi zaman altıdan az saydığı sorusu yanıtını buldu: sayacı sıra belirliyordu. Bir sonraki soru işin kaç parçaya bölüneceğidir. Çatallanan işçi süreçlerin sayısı bir ortam değişkeninden ya da çekirdek sayısından gelir; sonraki ders bu sayının nereden okunduğunu ve çatallanan sürecin belleğinin ebeveyne neden yansımadığını ölçer.

İ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