Ders 12 / 14
Bekleme Grupları
Beş işçinin tamamlanmasını bir sayaç bekliyor: Add başlamadan önce sayacı beşe çıkarıyor, her işçi kendi Done'unu defer ile garanti ediyor, Wait sayaç sıfıra inince dönüyor — durma sınıfı yine beklediği geldi. Sayaç hiç sıfırlanmazsa Wait hiçbir zaman dönmüyor ve bu sızıntı bir boolean'la ölçülüyor; Done bir kez fazladan çağrıldığında sayaç eksiye düşüyor ve bekleme grubu panikliyor, panik recover ile yakalanıp yalnız panik=true yazılıyor. Sınırlayıcı ölçüm: beş işçinin hangisinin önce bittiği hiç ölçülmüyor, yalnız hepsinin bittiği ölçülüyor — sonuçlar toplanıp sıralanarak basılıyor.
İçindekiler
Önceki ders tek bir kilidi bekleyen tek bir goroutine’i ölçtü. Ama paylaşılan durumu
koruyan tarafın kendisi çoğu zaman tek bir goroutine değil, bir grup: bir işi paylara
bölüp paralel yürüten birden çok goroutine, ve onların tamamının bitmesini bekleyen bir
ana goroutine. Kilit bu beklemeyi kurmuyor — kilit yalnız aynı anda kaç goroutine’in bir
kaynağa dokunabileceğini sınırlıyor, kaç goroutine’in bittiğini saymıyor. Bu derste
ölçülen araç bu sayımı yapan sync.WaitGroup, bir bekleme grubu.
Bir bekleme grubunu bir kanalla taklit etmek mümkün — her işçi bittiğinde bir kanala boş
bir değer gönderir, main de o kanaldan tam olarak işçi sayısı kadar değer okur. Önceki
konudaki işçi havuzu ve yayma-toplama dersleri gerçekten böyle yaptı: sonuç kanalının
kendisi hem değeri taşıyor hem de kaç sonucun geldiğini sayıyordu. Bekleme grubu bu iki
işi ayırıyor: kaç işçinin bittiğini saymak için ayrı bir sayaç, değeri taşımak için (bu
derste olduğu gibi) ayrı bir kanal ya da hiç kanal. Bu ayrım, işçilerin ürettiği bir
değer olmadığında — yalnız bir işin bittiğini bilmek yeterli olduğunda — daha az
düzenek gerektiriyor.
Sayaç Sözleşmesi
Bekleme grubu bir dilim, bir kanal ya da bir liste değil — içinde tek bir tamsayı sayaç
tutan bir tip. Sözleşmesi üç çağrıya dayanıyor: Add(n) sayacı n artırıyor, Done()
sayacı bir azaltıyor, Wait() sayaç sıfıra inene kadar bloklanıyor. Bu üç çağrının doğru
sırayla kullanılması bir kural: Add her zaman ilgili goroutine başlamadan önce
çağrılmalı, Done her goroutine’in her çıkış yolunda çalışmalı — bu yüzden defer ile
yazılıyor, tıpkı önceki dersteki Unlock gibi — ve Wait sayaç sıfıra inmeden asla
dönmüyor.
Addin goroutine başlamadan önce çağrılması bir üslup tercihi değil. Add yerine
goroutine’in kendi içinde, ilk satırında çağrılsaydı, mainin kendisi grup.Wait()e
o goroutine daha Addini hiç çalıştırmadan ulaşabilirdi — bu durumda sayaç henüz sıfır
olduğu için Wait hemen dönebilir, henüz başlamamış bir işi hiç beklemeden. Bu, kod
kaynağına bakarak fark edilmesi güç bir hata sınıfı, çünkü hangi sırayla çalışacağı
goroutine zamanlamasına bağlı ve bu ders böyle bir kurulumu bilerek koşturmuyor — sonuç
koşumdan koşuma değişebileceği için bu kursun belirlenimcilik kısıtına girerdi. Kural
bunun yerine kaynağın kendisinde okunabilir: örnekteki grup.Add(1) satırı, döngünün
her adımında, go sözcüğünden önce duruyor.
// main.go — sayac sozlesmesi, sifirlanmayan sayacta sizinti, sayacin sifirin altina inmesi
package main
import (
"fmt"
"runtime"
"sort"
"sync"
)
func main() {
fmt.Println("-- sayac sozlesmesi: Add once, Done her yolda, Wait sifirda doner --")
const isSayisi = 5
var grup sync.WaitGroup
tamamlanan := make(chan string, isSayisi)
agirliklar := []int{3, 1, 4, 1, 5}
for i := 0; i < isSayisi; i++ {
grup.Add(1)
go func(indeks, agirlik int) {
defer grup.Done()
toplam := 0
for j := 0; j < agirlik*1000; j++ {
toplam += j
}
tamamlanan <- fmt.Sprintf("is-%d (agirlik=%d, toplam=%d)", indeks, agirlik, toplam)
}(i, agirliklar[i])
}
grup.Wait()
fmt.Println("Wait dondu: durdu, beklediği geldi")
close(tamamlanan)
var raporlar []string
for r := range tamamlanan {
raporlar = append(raporlar, r)
}
sort.Strings(raporlar)
for _, r := range raporlar {
fmt.Println(r)
}
fmt.Println()
fmt.Println("-- sifirlanmayan sayac: Add var, Done hic cagrilmiyor --")
taban := runtime.NumGoroutine()
var sizanGrup sync.WaitGroup
sizanGrup.Add(1)
bitti := make(chan struct{})
go func() {
sizanGrup.Wait()
close(bitti)
}()
select {
case <-bitti:
fmt.Println("Wait dondu: beklenmiyordu")
default:
fmt.Println("Wait dondu mu: hayir, sayac hic sifirlanmadi")
}
fmt.Println("durmayan goroutine kaldi mi:", runtime.NumGoroutine() > taban)
fmt.Println()
fmt.Println("-- sayacin sifirin altina inmesi: panik --")
panikVarMi := calistirVeKurtar(func() {
var asiriGrup sync.WaitGroup
asiriGrup.Add(1)
asiriGrup.Done()
asiriGrup.Done()
})
fmt.Println("panik:", panikVarMi)
}
// calistirVeKurtar verilen islevi calistirir, panikledi mi diye dondurur; yigit izi basilmaz.
func calistirVeKurtar(islev func()) (panikVarMi bool) {
defer func() {
if recover() != nil {
panikVarMi = true
}
}()
islev()
return false
}
-- sayac sozlesmesi: Add once, Done her yolda, Wait sifirda doner -- Wait dondu: durdu, beklediği geldi is-0 (agirlik=3, toplam=4498500) is-1 (agirlik=1, toplam=499500) is-2 (agirlik=4, toplam=7998000) is-3 (agirlik=1, toplam=499500) is-4 (agirlik=5, toplam=12497500) -- sifirlanmayan sayac: Add var, Done hic cagrilmiyor -- Wait dondu mu: hayir, sayac hic sifirlanmadi durmayan goroutine kaldi mi: true -- sayacin sifirin altina inmesi: panik -- panik: true
PD4. Beş işçi beş farklı ağırlıkla (3, 1, 4, 1, 5) başlatılıyor, her biri kendi
toplamını hesaplayıp bir kanala yazıyor. grup.Wait() ancak beşi de kendi Donesunu
çağırdıktan sonra dönüyor — bu, kursun ilk dersinden beri işlenen durma sınıfının bir
örneği daha: mainin kendisi bir goroutine gibi bekliyor, beklediği şey (sayacın sıfıra
inmesi) geldiğinde duruyor. Bekleme grubu burada kilitten farklı bir sözleşme sunuyor:
kilit tek bir kaynağı korurken, bekleme grubu kaç işin bittiğini sayıyor. Ortak
tanımın üçüncü iddiası bunu doğrudan destekliyor: beş “beklediği geldi” satırı beş ayrı
düzenekle kurulmuştu (tamponsuz alım, kapanan kanal, kilit, bekleme grubu, select) ve
düzenek değişse de bağımlılık değişmiyordu. Önceki ders kilidi, bu ders bekleme grubunu
kullanıyor — ikisi de aynı ailenin, “başka bir goroutine’in adımına bağlı olarak durma”
ailesinin, birer üyesi.
Ağırlıkların kendisi de bilerek farklı seçildi: 1 ağırlığı iki kez tekrarlanıyor
(is-1 ve is-3), geri kalan üçü (3, 4, 5) birbirinden farklı. Bu, bekleme grubunun
işçileri hiçbir şekilde ayırt etmediğini gösteriyor — sayaç yalnız kaç Done
çağrıldığını tutuyor, hangi goroutine’in hangi işi yaptığını bilmiyor. is-1 ile is-3
aynı ağırlıkla aynı toplamı (499500) üretiyor ve bekleme grubu için bu iki işçi
arasında hiçbir fark yok; ayrımı yalnız çıktıdaki is- önekleri taşıyor, bekleme grubunun
kendisi değil.
PD5. Beş işçi beş farklı sürede bitiyor — ağırlığı yüksek olan daha uzun döngü
çalıştırıyor — ama bu ders hangisinin önce bittiğini hiç ölçmüyor. Kanala yazılan
raporlar tamamlanan kanalından okunup bir dilime toplanıyor, sonra sort.Strings ile
sıralanıp basılıyor; kanaldan geliş sırası hiç yazdırılmıyor, çünkü o sıra goroutine
zamanlamasına bağlı ve koşumdan koşuma değişebilir. Bekleme grubunun garanti ettiği tek
şey sayacın sıfıra indiği an; hangi Done çağrısının önce geldiği bir garanti değil. Bu,
dersin sınırlayıcı ölçümü: beş işçinin hepsi bitti ölçülüyor, hangisi önce bitti
ölçülemiyor — ölçülmeye çalışılsaydı bu dersin de çıktısı koşumdan koşuma değişir ve
doğrulama sapma verirdi.
grup değişkeni mainin yerel bir değişkeni ve her goroutine’e bir kapanış üzerinden
işaretçi olmadan erişiliyor — Go’nun kapanışları çevreleyen değişkenleri kendi
adresleriyle yakaladığı için grup.Add, grup.Done ve grup.Wait hep aynı sayaca
dokunuyor. Bekleme grubu bir yapı ve Go Temelleri kursunda ölçülen değerle aktarım
kuralı burada da geçerli: grupün bir kopyası alınıp bir işleve değerle geçirilseydi,
o kopyanın kendi sayacı olurdu ve mainin kendi sayacından bağımsız kalırdı — sözleşme
sessizce bozulurdu. İncelemenin görevi (Go Komut Ailesi dersinde ölçülen inceleyicinin
işi) tam olarak bunun gibi kalıpları yakalamak; bu ders böyle bir kopyalamayı bilerek
kurup koşturmuyor, çünkü bu, önceki dersteki kilidin dışına taşan tek satır gibi, kaynağı
okuyarak fark edilmesi güç bir hata sınıfı olurdu.
Kilit ile bekleme grubu burada iki ayrı sorumluluğu paylaşıyor. Önceki dersteki
KorumaliSayac, aynı değere birden çok goroutine’in aynı anda yazmasını engellemeyi
üstlenmişti; bu değeri kim koruyorsa koruyor olsun, “hepsi bitti mi” sorusuna yanıt
vermiyordu. Bu dersteki tamamlanan kanalı ile grup de tersini yapıyor: hangi işçinin
işi bittiğini sayıyor, ama işçilerin kendi aralarında paylaştığı bir belleğe hiç
karışmıyor — beş işçi burada zaten hiçbir ortak değişkene yazmıyor, her biri kendi yerel
toplamını hesaplayıp kanala gönderiyor. Gerçek bir programda bu iki araç birlikte
görülüyor: birden çok goroutine ortak bir değeri kilitle koruyup her biri işini bitirince
Done çağırabiliyor, main da Wait ile hepsinin bitmesini bekleyip ancak o zaman
korunan değeri okuyabiliyor. Kilit neyin korunacağını, bekleme grubu ne zaman
okunmaya hazır olduğunu belirliyor; bu iki soru birbirinin yerini tutmuyor.
Sıfırlanmayan Sayaç: Sızıntı
PD6. İkinci kurulum sözleşmeyi bilerek yarım bırakıyor: sizanGrup.Add(1) çağrılıyor
ama karşılığındaki Done hiçbir goroutine’de hiç çağrılmıyor. Sayaç bir daha asla sıfıra
inmeyeceği için sizanGrup.Wait()i çağıran goroutine sonsuza dek bloklu kalıyor — bu,
dördüncü durma sınıfının kendisi: hiç durmadı. Bu bloklanmayı beklemeden denetlemek
için Waiti ayrı bir goroutine içine alıp bir kapatma kanalıyla sarıyoruz; selectin
default dalı, o kanaldan hiçbir zaman bir değer gelmeyeceğini — çünkü sayaç matematiksel
olarak asla sıfıra inemez — anında ve kesin biçimde gösteriyor. Bu bir zamanlama tahmini
değil: default dalının seçilmesi hiçbir koşumda değişmiyor, çünkü Waitin dönmesi
zamanlamaya değil sayacın değerine bağlı ve o değer hiç değişmiyor.
PD7. runtime.NumGoroutine() çağrısı taban sayıyla karşılaştırılıp yalnız bir
true/false üretiyor; kaç goroutine’in kaldığı ayrıca sayılmıyor. Bu, ortak tanımın
ikinci iddiasının bir başka örneği: durmamak Go çalışma zamanı tarafından hiçbir hata ya
da uyarıya dönüştürülmüyor, programın geri kalanı normal biçimde çalışmaya devam ediyor —
yalnız o tek goroutine hiçbir zaman bitmiyor. Bu sessizlik, Goroutine dersinde ölçülen
kanal sızıntılarıyla aynı aileden: bekleme grubu da kanal da, karşı tarafın sözleşmesini
yerine getirmemesi durumunda kendiliğinden bir hata üretmiyor.
Bekleme grubunun kendisinde bir iptal mekanizması yok — Wait çağrısını dışarıdan
durdurmanın bir yolu bulunmuyor, sayaç sıfıra inene kadar bekler. Bağlam paketinin
zaman aşımı dersinde ölçülen ctx.Done() deseni bu boşluğu dolduruyor, ama bekleme
grubunun kendisi böyle bir çıkış sunmuyor; bu yüzden gerçek bir programda sonsuz
bekleme riski taşıyan bir Wait çağrısı, çoğunlukla bir zaman aşımıyla birlikte, ayrı bir
goroutine ve kanal üzerinden — tam olarak bu derste bitti kanalıyla yapıldığı gibi —
sarmalanıyor. Bu ders o sarmalamayı yalnızca sızıntıyı denetlemek için kullandı; gerçek bir
zaman aşımı kurup beklemeyi kesmek B payının konusuydu ve burada tekrarlanmıyor.
Sayacın Sıfırın Altına İnmesi: Panik
PD8. Üçüncü kurulum tam tersi bir hatayı modelliyor: Add(1) bir kez çağrılıyor ama
Done() iki kez çağrılıyor. Bekleme grubu bunu sessizce görmezden gelmiyor — sayaç
negatife düştüğü anda panikliyor, çünkü negatif bir sayaç kütüphanenin kendi iç
tutarlılığını bozuyor ve bu durumu susturmak yerine hemen bildirmek Go standart
kütüphanesinin seçimi. calistirVeKurtar, önceki derslerin kalıbını sürdürerek paniği
recover ile yakalıyor ve yalnız panik=true/panik=false yazıyor; panik metninin
kendisi hiçbir yerde basılmıyor, çünkü o metin bir yığıt izi taşıyor.
Panik metninin basılmaması yalnız bir yığıt izi kaygısı değil. Standart kütüphanenin bu
paniğe verdiği metin, kaynağı derleyen sürüme bağlı olarak değişebilir — kelimesi kelimesine
aynı kalacağının garantisi yok, çünkü bu metin dilin belirttiği bir sözleşmenin parçası
değil, kütüphanenin iç uygulamasının bir ayrıntısı. panik=true/panik=false bunun
yerine dilin garanti ettiği tek şeyi ölçüyor: sayaç negatife düştüğünde bir panik
oluyor mu, olmuyor mu. Kanal Kapatma ve Aralık dersinde kapalı bir kanala ikinci kez
gönderim yapmanın da aynı biçimde ölçülmesi bu kursun tekrar eden bir alışkanlığı: paniğin
kendisi ölçülüyor, paniğin metni değil.
Bu iki hatalı kurulum ilginç bir karşıtlık kuruyor: sayacın hiç sıfırlanmaması sessiz
kalıyor, sayacın fazla sıfırlanması ise yüksek sesle bildiriliyor. Bu fark bekleme
grubunun kendi tasarımından geliyor, iki hatanın önemine dair bir yargı değil. Sayaç
sıfırın altına inebilir çünkü bekleme grubu her Done çağrısında sayacı denetliyor ve
negatif bir değeri hemen yakalıyor; sayacın hiç sıfırlanmaması ise bekleme grubunun
denetleyebileceği bir durum değil — kütüphane, kaç Add çağrısının karşılıksız kaldığını
bilemez, çünkü “karşılıksız kalmak” sonsuza kadar sürebilecek bir bekleyiş, bir hata değil.
Panik anlık bir olay olduğu için yakalanabiliyor; sızıntı bir olay değil, bitmeyen bir
durum olduğu için yakalanamıyor, yalnız dışarıdan denetlenebiliyor.
Bu üç kurulum bekleme grubunun sözleşmesini üç yönden sınıyor: sayaç doğru sıfırlandığında
Wait durur, hiç sıfırlanmadığında sonsuza dek durmuyor, fazla sıfırlandığında panikliyor.
Sözleşmenin üç ihlali de aynı kökten geliyor — Add ile Done çağrılarının sayısının
birbirine denk düşmesi gerekiyor, ve bu denkliği derleyici değil çalışma zamanı
denetliyor. Önceki derste kilidin sözleşmesi neyin kilit altında kalması gerektiğiydi;
burada sözleşme kaç Addin kaç Donea karşılık geldiği. İkisi de derleyicinin hiç
göremediği bir kural: bir kaynağı okuyan biri, Add ile Done çağrılarının sayıca eşit
olup olmadığını yalnız kaynağı izleyerek, çalıştırarak görebiliyor.
Özet
sync.WaitGroup,Add,DoneveWaitüçlüsüyle çalışan bir sayaç;Waitsayaç sıfıra inene kadar bloklanıyor ve durma sınıfı beklediği geldi.- Beş işçinin tamamlanma sırası hiç ölçülmüyor, yalnız hepsinin bittiği ölçülüyor; sonuçlar bir kanaldan toplanıp sıralanarak basılıyor.
- Sayaç hiç sıfırlanmazsa
Waitsonsuza dek bloklanıyor; bu, engellenmedenselectindefaultdalıyla denetlendi ve sızıntı yalnız bir boolean olarak ölçüldü. - Sayacı sıfırın altına indiren fazladan bir
Doneçağrısı bekleme grubunu paniklettiriyor; panikrecoverile yakalandı, yalnızpanik=trueyazıldı. - Kilidin sözleşmesi neyin korunacağıydı, bekleme grubunun sözleşmesi kaç
Addin kaçDonea karşılık geldiği — ikisi de derleyicinin denetlemediği, yalnız koşturarak görülebilen kurallar.
Sonraki Adım
Bekleme grubu bir sayı sayıyor ama tek başına hiçbir değeri korumuyor ve hiçbir işlemi tek seferle sınırlamıyor. İki ders boyunca paylaşılan durumu korumanın tek aracı kilitti; sıradaki ders bu tekelin doğru olmadığını gösteriyor ve paylaşılan durumun iki ayrı ilkelini ölçüyor: bir gövdenin kaç goroutine çağırırsa çağırsın yalnız bir kez çalışmasını garanti eden bir kez çalıştırma, ve tek bir değişkeni kilit kullanmadan koruyan atomik işlemler. Her iki ilkel de kendi dar sözleşmesiyle geliyor — biri yalnız bir defalık bir kurulumu, öbürü yalnız tek bir değişkeni koruyor — ve bu darlığın nerede yeterli, nerede yetersiz kaldığı sıradaki dersin kendi sınırlayıcı ölçümü.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.