Ders 07 / 14
Yayma ve Toplama
Bir kaynak, sabit sayıda tüketiciye yayıldığında (yayma) sonuçlar tek bir toplama kanalında yeniden birleşiyor (toplama); toplayıcının durma sebebi, bir bekleme grubunun bütün üreticiler bitince toplama kanalını kapatmasına bağlı. Sınırlayıcı ölçüm: toplama kanalını kim kapatır sorusunun tek yanıtı vardır — hiçbir üretici tek başına güvenle kapatamaz (deneyen üç üreticiden ikisi paniklerken biri başarır) ve kapatılmazsa toplayıcı hiç durmaz.
İçindekiler
Önceki ders, sabit sayıda işçinin aynı iş kanalını dinlediği bir kurulumu ölçtü: tek kaynak, çok tüketici, ve durma sinyali hepsine tek bir kapatmayla ulaşıyordu. Ama iş her zaman işçilerin kendi başına yetiştirebileceği hızda gelmiyor. Bazen üretilen değerlerin kendisi birden fazla işlemciye dağıtılmalı — bu yayma — ve o dağıtılmış işin sonuçları tekrar tek bir akışta toplanmalı — bu toplama. Önceki dersin sorduğu “kanalı kim kapatır” sorusu burada da geçerli, ama yanıtı farklı bir yerde duruyor: işçi havuzunda kapatan taraf işi dağıtan tarafken, burada kapatılması gereken kanala birden fazla taraf yazıyor — ve bu, “kim kapatır” sorusunu ilk kez gerçek bir soruna çeviriyor.
Yayma ve toplama iki ayrı yönü kapsıyor. Yayma, bir üretim kaynağının (bir kuyruk, bir kanal) birden fazla işlemciye açılması — önceki dersteki iş kanalıyla neredeyse aynı düzenek, çünkü paylaşılan bir kanaldan okuyan sabit sayıda goroutine zaten bir yaymadır. Toplama ise tersi yönde çalışıyor: birden fazla kaynağın tek bir kanalda birleşmesi. İki yön birlikte kurulduğunda ortaya yeni bir soru çıkıyor — yaymada kapatan taraf bellidir (kaynağı yöneten tek taraf), ama toplamada kapatılması gereken kanala kaç tarafın yazdığı, kapatma sorumluluğunu da o kadar taraf arasında paylaştırma riski taşıyor.
Bir Kaynağın Çoğa Yayılması, Sonuçların Tek Kanalda Toplanması
Aşağıdaki ölçüm altı değeri üç üretici goroutine’ine yayıyor; her üretici kendi payına düşen
değerleri kareleyip tek bir toplama kanalına yazıyor. Toplama kanalını kimin kapatacağı
burada zaten çözülmüş: ayrı bir goroutine, bir bekleme grubunun (sync.WaitGroup) bütün
üreticilerin bittiğini bildirmesini bekliyor ve ancak o zaman toplamayı kapatıyor. Tek
sahiplik burada bir üreticide değil, üreticilerin dışında kuruluyor.
// yaymatoplama.go — kaynak uc uretene yayiliyor, sonuclar tek kanalda toplaniyor; toplama kanalini bekleme grubu kapatiyor
package main
import (
"fmt"
"sort"
"sync"
)
func uretici(girdi <-chan int, toplama chan<- int, grup *sync.WaitGroup) {
defer grup.Done()
for deger := range girdi {
toplama <- deger * deger
}
}
func main() {
const isSayisi = 6
const ureticiSayisi = 3
girdi := make(chan int, isSayisi)
toplama := make(chan int, isSayisi)
var grup sync.WaitGroup
for id := 1; id <= ureticiSayisi; id++ {
grup.Add(1)
go uretici(girdi, toplama, &grup)
}
for i := 1; i <= isSayisi; i++ {
girdi <- i
}
close(girdi)
// Toplayici: butun ureticiler bitince toplama kanalini KENDISI kapatiyor;
// hicbir uretici kendi basina kapatmiyor.
go func() {
grup.Wait()
close(toplama)
}()
fmt.Println("-- kaynak 3 uretene yayiliyor, sonuclar tek kanalda toplaniyor --")
sonuclar := make([]int, 0, isSayisi)
for s := range toplama {
sonuclar = append(sonuclar, s)
}
sort.Ints(sonuclar)
toplamDeger := 0
for _, s := range sonuclar {
toplamDeger += s
}
fmt.Printf("yayilan is sayisi: %d\n", isSayisi)
fmt.Printf("toplanan sonuc sayisi: %d, toplam: %d\n", len(sonuclar), toplamDeger)
fmt.Println("toplayicinin durma sebebi: bekledigi geldi (toplama kanali kapandi)")
}
-- kaynak 3 uretene yayiliyor, sonuclar tek kanalda toplaniyor -- yayilan is sayisi: 6 toplanan sonuc sayisi: 6, toplam: 91 toplayicinin durma sebebi: bekledigi geldi (toplama kanali kapandi)
KL6. girdi kanalı önceki dersteki isler ile aynı rolü oynuyor — üç üretici arasında
paylaşılan bir kuyruk. Farklı olan toplama kanalı: üç üretici de aynı anda ona yazıyor,
tek bir işçi değil. Sonuç sayısının altıya, toplamın doksan bire (kareler 1+4+9+16+25+36)
çıkması hangi üreticinin hangi değeri işlediğinden bağımsız; sıralama (sort.Ints) da bu
yüzden gerekiyor — üç üreticinin toplama kanalına yazma sırası koşumdan koşuma değişebilir,
ama kümenin kendisi değişmiyor.
Bu ölçümde uretici işlevi, önceki dersteki isci işleviyle neredeyse aynı gövdeye sahip:
ikisi de bir girdi kanalından okuyor, bir çıktı kanalına yazıyor. Aradaki fark işlevin kendi
kodunda değil, çağrıldığı bağlamda — isci sabit sayıda tüketiciyi aynı işi paylaşacak
biçimde kurarken, uretici aynı işlevi bu kez birden fazla yazıcının aynı hedefe yazması
bağlamında çalıştırıyor. Kalıbın adı değişiyor (işçi havuzundan yaymaya), ama alttaki kanal
düzeneği aynı kalıyor; değişen yalnız kapatma sorumluluğunun nerede durduğu.
KL7. Kapatan goroutine burada üçüncü bir taraf: ne üreticilerden biri, ne toplayıcının
kendisi. grup.Wait() üç üreticinin üçü de grup.Done()e ulaşana kadar dönmüyor, ve ancak
o zaman close(toplama) çalışıyor — bu, toplama kanalına yapılacak son yazmanın zaten
gerçekleşmiş olduğunu garanti ediyor. girdi ve toplama kanallarının ikisi de isSayisi
kapasitesiyle tamponlu açılıyor, aynı önceki dersteki gerekçeyle: üreticiler kendi paylarını
alıp yazarken engellenmeden çalışabilsin diye. Bu üçüncü taraf yaklaşımı bir sonraki bölümün
sorusunu doğuruyor: ya bu ayrı goroutine hiç olmasaydı, ve üreticilerin her biri kendi işini
bitirince kanalı kendisi kapatmaya çalışsaydı?
Toplama Kanalını Kim Kapatır
Bir kanalı kapatmak Go’da tek seferlik bir işlem: aynı kanalı iki kez kapatmak panikler.
Üç üretici de aynı kanalı paylaşıyorsa ve her biri kendi işini bitirince “ben bittim, kanalı
kapatayım” derse, ilk kapatan başarılı olur ama ondan sonraki her kapatma denemesi panikler.
Aşağıdaki ölçüm bunu kuruyor: her üretici önce kendi değerini gönderiyor, sonra (bir bekleme
noktasıyla bütün üreticilerin göndermeyi bitirdiğinden emin olunduktan sonra) kendi başına
close(toplama) deniyor.
// naifkapatma.go — her uretici kendi basina kapatmayi denerse, ilkinden sonrakiler panikler
package main
import (
"fmt"
"sync"
)
func ureticiNaif(id int, toplama chan int, senkron *sync.WaitGroup, sonuc chan<- bool, bitis *sync.WaitGroup) {
defer bitis.Done()
toplama <- id
senkron.Done()
senkron.Wait() // butun ureticiler kendi degerini gonderene kadar bekle
panikledi := false
func() {
defer func() {
if r := recover(); r != nil {
panikledi = true
}
}()
close(toplama)
}()
sonuc <- panikledi
}
func main() {
const ureticiSayisi = 3
toplama := make(chan int, ureticiSayisi)
sonuc := make(chan bool, ureticiSayisi)
var senkron sync.WaitGroup
var bitis sync.WaitGroup
senkron.Add(ureticiSayisi)
bitis.Add(ureticiSayisi)
for id := 1; id <= ureticiSayisi; id++ {
go ureticiNaif(id, toplama, &senkron, sonuc, &bitis)
}
bitis.Wait()
close(sonuc)
basarili, panikleyen := 0, 0
for panikledi := range sonuc {
if panikledi {
panikleyen++
} else {
basarili++
}
}
fmt.Println("-- her uretici once kendi degerini gonderiyor, sonra kapatmayi deniyor --")
fmt.Printf("basariyla kapatan uretici sayisi: %d / %d\n", basarili, ureticiSayisi)
fmt.Printf("panikleyen uretici sayisi: %d / %d\n", panikleyen, ureticiSayisi)
}
-- her uretici once kendi degerini gonderiyor, sonra kapatmayi deniyor -- basariyla kapatan uretici sayisi: 1 / 3 panikleyen uretici sayisi: 2 / 3
KL8. Sonuç şaşırtıcı olmayan bir biçimde tek yanıtlı: her koşumda tam olarak bir
üretici başarıyla kapatıyor, kalan ikisi panikliyor. Hangi üreticinin başardığı koşumdan
koşuma değişebilir (üç üretici de kapatmayı aynı anda denemeye başlıyor), ama kaç
üreticinin başardığı hiç değişmiyor — Go’nun çalışma zamanı bir kanalın kapatılmasını iç
kilitle koruyor, bu yüzden birden fazla goroutine aynı anda close çağırsa bile yalnız biri
kanalı gerçekten kapatıyor, geri kalanı zaten kapanmış bir kanalı kapatmaya çalıştığını görüp
panikliyor. Ders bu yüzden kimin başardığını değil, kaçının başardığını yazdırıyor.
Bu iç kilit, closein kendisinin atomik bir işlem olduğu anlamına geliyor: bir kanalın
“kapalı mı değil mi” durumu, birden fazla goroutine aynı anda sorup değiştirmeye çalışsa bile,
her zaman tek bir sıralı geçmişe sahip. Üç goroutine close(toplama)yı gerçekten aynı anda
çağırsa da çalışma zamanı bunları görünmez bir sırayla işliyor — ilk işlenen kapatma
gerçekleşiyor, ondan sonraki her biri zaten kapalı bir kanalı bulup panikliyor. Bu davranış
paylaşılan bir değişkene korumasız yazmaktan çok farklı: paylaşılan bir sayaca aynı anda iki
goroutine yazarsa sonuç tanımsız bir yarış koşuludur (ölçülmüyor, yalnız kavram olarak
M01/K05 İşletim Sistemi Kavramları’nda kuruldu), ama kanal kapatma dilin kendisinin garanti
ettiği, yarışa açık olmayan bir işlem — kazanan kim olursa olsun tam olarak bir kazanan
çıkıyor.
KL9. İkinci senkronizasyon noktası (senkron) burada bilinçli bir tasarım kararı:
ölçüm her üreticinin önce kendi değerini gönderdiğinden, sonra kapatmayı denediğinden emin
oluyor. Bu ayrım olmasaydı, bir üretici henüz kendi değerini göndermeden bir başkası kanalı
kapatabilir, ve o üreticinin gönderimi (toplama <- id) kapatma satırından önce değil
panik olarak sonuçlanırdı — bu da hangi hattın hangi sebeple paniklediğini karıştırırdı.
senkron.Wait(), önceki derste görülen grup.Wait() gibi, sırayı çizelgeleyiciye bırakmadan
iki aşamayı (gönderme, kapatma) birbirinden ayırıyor.
Bu ayrımın kendisi bir gözlemi gizliyor: bir kapalı kanalla ilgili panik aslında iki ayrı
biçimde tetiklenebiliyor — kapalı bir kanalı tekrar kapatmak (close of closed channel) ve
kapalı bir kanala göndermek (send on closed channel). Yukarıdaki senkron engeli olmasaydı,
bir üretici henüz kendi değerini göndermeden (toplama <- id) bir başkası kanalı kapatmış
olabilirdi, ve o gönderim satırı kendi recover bloğunun dışında kaldığı için panik
yakalanmadan program çöker, ders kod_dogrula.pyye hiç ulaşmadan durur. senkron.Wait()
engeli tam olarak bunu önlüyor: bütün gönderimlerin bittiğinden emin olunmadan hiçbir üretici
kapatmayı denemiyor, bu yüzden gözlenen panik türü hep aynı (kapalı kanalı tekrar kapatma),
hiçbir zaman diğeri değil.
Bu ölçüm önceki bölümdeki doğru düzenin neden doğru olduğunu gösteriyor: toplama kanalını kapatma sorumluluğu üreticilerin hiçbirine ait değil, çünkü hiçbiri diğerlerinin ne zaman biteceğini bilmiyor. Sorumluluk, ancak bütün üreticilerin bittiğini bilen üçüncü bir tarafa (bir bekleme grubunu bekleyen ayrı bir goroutine’e) verilebiliyor.
Kapatılmayan Toplama Kanalı
Önceki iki kurulum, kapatma sorumluluğunun sıfır ile birden fazla taraf arasında
paylaştırılmasının iki farklı sonucunu gösterdi: birinci kurulumda sorumluluk tam olarak bir
tarafa aitti ve her şey sessizce bitti; ikinci kurulumda sorumluluk (yanlışlıkla) üç tarafa
aitti ve iki panik ortaya çıktı. Geriye üçüncü olasılık kalıyor: sorumluluk hiç kimseye
verilmezse ne olur? Üçüncü taraf goroutine (yukarıdaki bekleme grubunu bekleyip toplama
kanalını kapatan ayrı goroutine) hiç kurulmazsa, toplama kanalı hiç kapanmıyor. Aşağıdaki
ölçüm bunu en yalın
haliyle kuruyor: bir toplayıcı goroutine toplama kanalında aralık yapıyor, ama kanalı
kapatacak hiç kimse yok.
// sizintitoplama.go — toplama kanali hic kapatilmazsa toplayici hic durmaz
package main
import (
"fmt"
"runtime"
)
func main() {
taban := runtime.NumGoroutine()
toplama := make(chan int)
go func() {
// Kimse toplama kanalini kapatmadigi icin bu aralik hic bitmeyecek.
for range toplama {
}
}()
fmt.Println("-- toplama kanali hic kapatilmadi --")
fmt.Printf("durmayan toplayici kaldi mi: %v\n", runtime.NumGoroutine() > taban)
}
-- toplama kanali hic kapatilmadi -- durmayan toplayici kaldi mi: true
KL10. Bu, önceki dersteki kapatılmayan iş kanalı ölçümünün toplama tarafındaki aynası:
orada işçiler sonsuza kadar bekliyordu, burada tek bir toplayıcı sonsuza kadar bekliyor. Fark,
sızıntının yerinde: işçi havuzunda sızan taraf çok sayıda tüketiciydi, burada sızan taraf
tek bir toplayıcı — ama ikisinde de sebep aynı, kimsenin kapatmadığı bir kanalda aralık.
Toplayıcının kendisi de bir goroutine olduğu için, onun sızması da işçi sızıntısından farklı
bir araçla değil, aynı runtime.NumGoroutine() karşılaştırmasıyla ölçülüyor.
Bu üç kurulumun ortak noktası, hiçbirinin sorunu kaynağında çözmemesi: doğru kurulum bile
bütün üreticilerin sonunda biteceğini varsayıyor. Üreticilerden biri kendi işini hiç
bitirmezse (örneğin sonsuz bir döngüye girerse), grup.Wait() de hiç dönmez ve toplama kanalı
yine hiç kapanmaz — bu kurulum kapatma sorumluluğunu doğru bir tarafa veriyor, ama o tarafın
kendisi de bir koşula bağlı kalıyor. Bir üreticinin kendi işini bitirmemesi olasılığını
sınırlamak (belirli bir süre ya da dışarıdan gelen bir sinyalle) bu dersin konusu değil;
o soruyu bağlam paketiyle iptal yayma ölçen bir sonraki ders ele alıyor.
Üç ölçüm birlikte konunun tezini tamamlıyor: doğru kurulan toplama (bir kişide toplanan sorumluluk) belirlenimci ve sessiz durdu; yanlış kurulan toplama (herkesin kendi başına kapatmaya çalışması) belirlenimci ama gürültülü durdu — paniklerle; ve hiç kurulmayan toplama (kimsenin kapatmaması) hiç durmadı. Üçü de aynı sorunun farklı yanıtları: toplama kanalını kapatma sorumluluğu tam olarak bir tarafa ait olmalı, ne az ne çok. Bu ilke işçi havuzundaki tek kapatan tarafla (işi dağıtan taraf) aynı ilkenin bir genellemesi: kaç taraf bir kanala yazarsa yazsın, kapatma her zaman ayrı ve tekil bir sorumluluk olarak kalıyor.
Özet
- Bir kaynak sabit sayıda üreticiye yayıldığında (yayma), sonuçlar tek bir toplama kanalında yeniden birleşiyor (toplama); toplanan sonuç sayısı ve toplamı üretici sayısından bağımsız iki sabit değer.
- Toplama kanalını kapatma sorumluluğu üreticilerin hiçbirine ait değil — bir bekleme grubunun bütün üreticilerin bittiğini bildirdiği üçüncü bir taraf kapatıyor.
- Her üretici kendi başına kapatmayı denerse, tam olarak bir tanesi başarır, kalanı panikler; hangi üreticinin başardığı değişse de kaçının başardığı hiç değişmiyor.
- Toplama kanalı hiç kapatılmazsa toplayıcı sonsuza kadar bekler; sızıntı yine yalnız bir boolean olarak ölçülüyor.
- Kapatma sorumluluğunun tam olarak bir tarafa ait olması gerekiyor: sıfır taraf sızıntıya, birden fazla taraf paniğe yol açıyor.
- Kanal kapatma dilin kendisinin garanti ettiği atomik bir işlem; birden fazla goroutine aynı
anda
closeçağırsa bile tam olarak biri başarır, geri kalanı panikler.
Sonraki Adım
Bu derste bir kaynak çoğa yayıldı ve sonuçlar tek bir kanalda toplandı — ama akış tek bir adımdı: her üretici girdiyi aldı, kareledi, sonucu yazdı. Gerçek bir işleme genellikle birden fazla adımdan geçiyor ve her adımın kendi hızı, kendi kapatma sorumluluğu var. Sıradaki ders bu adımları zincirliyor — aşamalı akış bir boru hattı kuruyor, ve her aşama kendi çıktı kanalını kendisi kapatıyor. Soru bu sefer tek bir kapatmanın kimde durduğu değil: zincirdeki bir aşama erken durursa, önündeki aşamalara ne oluyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.