İçeriğe geç
academia.sh

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, Done ve Wait üçlüsüyle çalışan bir sayaç; Wait sayaç 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 Wait sonsuza dek bloklanıyor; bu, engellenmeden selectin default dalı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; panik recover ile yakalandı, yalnız panik=true yazı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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat