Ders 06 / 14
İşçi Havuzları
Sabit sayıda işçi, paylaşılan bir iş kanalından okur; kanal kapandığında hepsi aynı sebeple durur — beklediği geldi. Beş iş sekiz işçiye dağıtıldığında toplam alınan iş ve toplam rapor eden işçi sayısından, en az kaç işçinin hiç iş almadan durduğu çıkarılır. Sınırlayıcı ölçüm: işçi sayısını artırmak var olan işi bölmez, yalnız daha fazla işçinin boş kalmasına yol açar; kapatılmayan bir iş kanalında işçiler hiç durmaz.
İçindekiler
Önceki konu kapatmanın bir sonlanma sinyali olduğunu ve bu sinyali göndermenin sorumluluğunun
gönderende durduğunu ölçtü — kapanan bir kanaldan alım sıfır değer ve false veriyordu, kanalda
aralık biterken. O ölçüm tek bir alıcıyla çalıştı. Bu ders aynı sinyali çok sayıda goroutine
arasında kullanan ilk kalıba bakıyor: sabit sayıda işçi, tek bir iş kanalını paylaşıyor ve
kanal kapandığında hepsi aynı anda değil ama aynı sebeple duruyor.
İşçi havuzu deseninin kendisi basit: bir iş kanalı, bir sonuç kanalı, ve sabit sayıda işçi goroutine’i. Her işçi iş kanalından okur, işi işler, sonucu sonuç kanalına yazar; iş kanalı kapandığında okuma döngüsü biter. Yeni olan şey, önceki derste tek bir alıcının gördüğü sonlanma sinyalinin şimdi birden fazla goroutine’e aynı anda ulaşması — ve her birinin bu sinyale bağımsızca tepki vermesi. Bu dersin sorusu: sabit sayıda işçi bu paylaşılan sinyali gördüğünde, kaçı hangi işi yapmış olarak duruyor, ve işçi sayısı iş sayısını aştığında ne oluyor?
Havuzun kendisi tek bir düşünceye dayanıyor: işçi sayısı sabit tutulursa, kaynak kullanımı da sabit kalır — kaç iş gelirse gelsin, aynı anda çalışan goroutine sayısı hiç değişmiyor. Bu, her iş için ayrı bir goroutine başlatmanın (bir öncekinde görülmeyen ama akla gelebilecek bir alternatif) tam tersi bir tasarım kararı: iş sayısı denetimsiz büyüdüğünde goroutine sayısı da sınırsız büyümesin diye, işçi sayısı en baştan sabitleniyor ve iş kanalı bu sabit sayı ile büyüyen iş kümesi arasında bir tampon görevi görüyor. Bu tamponun nasıl tükendiğini — hangi işçinin ne kadar aldığını değil, hepsinin aynı anda mı yoksa farklı sebeplerle mi durduğunu — ölçmek bu dersin konusu.
Sabit Sayıda İşçi, Paylaşılan Bir Kuyruk
Aşağıdaki ölçüm beş işi sekiz işçiye dağıtıyor. Her işçi aynı isler kanalından okuyor,
kareledigi değeri sonuclar kanalına yazıyor ve kendi payına düşen iş bittiğinde — yani
isler kanalı kapanıp döngüsü sona erdiğinde — bir rapor gönderiyor. Rapor tek bir sebep
taşıyor: beklediği geldi. Hiçbir işçi kendi kararıyla durmuyor; hepsi aynı olayı, kanalın
kapanmasını, bekliyor.
// havuz.go — sabit sayida isci, paylasilan bir is kanalindan okur; kanal kapaninca hepsi ayni sebeple durur
package main
import (
"fmt"
"sort"
"sync"
)
func isci(id int, isler <-chan int, sonuclar chan<- int, raporlar chan<- string, grup *sync.WaitGroup) {
defer grup.Done()
for is := range isler {
sonuclar <- is * is
}
// Bekleme grubu (sync.WaitGroup) burada arac olarak kullaniliyor; kendi dersi
// kursun ilerleyen bir konusunda.
raporlar <- "bekledigi geldi"
}
func main() {
const isSayisi = 5
const isciSayisi = 8
isler := make(chan int, isSayisi)
sonuclar := make(chan int, isSayisi)
raporlar := make(chan string, isciSayisi)
var grup sync.WaitGroup
for id := 1; id <= isciSayisi; id++ {
grup.Add(1)
go isci(id, isler, sonuclar, raporlar, &grup)
}
for i := 1; i <= isSayisi; i++ {
isler <- i
}
close(isler)
grup.Wait()
close(sonuclar)
close(raporlar)
fmt.Println("-- 5 is, 8 isci: kapatilan is kanali --")
raporSayisi := 0
tumuGeldiMi := true
for r := range raporlar {
raporSayisi++
if r != "bekledigi geldi" {
tumuGeldiMi = false
}
}
sonucListesi := make([]int, 0, isSayisi)
for s := range sonuclar {
sonucListesi = append(sonucListesi, s)
}
sort.Ints(sonucListesi)
toplam := 0
for _, s := range sonucListesi {
toplam += s
}
fmt.Printf("toplam alinan is (tum iscilerin toplami): %d\n", len(sonucListesi))
fmt.Printf("toplam rapor eden isci sayisi: %d\n", raporSayisi)
fmt.Printf("hepsi ayni sebeple mi durdu (bekledigi geldi): %v\n", tumuGeldiMi)
fmt.Printf("en az bos kalan isci sayisi (%d - %d): %d\n", isciSayisi, isSayisi, isciSayisi-isSayisi)
fmt.Printf("toplanan sonuc sayisi: %d, toplam: %d\n", len(sonucListesi), toplam)
}
-- 5 is, 8 isci: kapatilan is kanali -- toplam alinan is (tum iscilerin toplami): 5 toplam rapor eden isci sayisi: 8 hepsi ayni sebeple mi durdu (bekledigi geldi): true en az bos kalan isci sayisi (8 - 5): 3 toplanan sonuc sayisi: 5, toplam: 55
Ölçümün kendisi de sırayı çizelgeleyiciye bırakmıyor. sonuclar ve raporlar kanalları
grup.Wait() dönmeden önce kapatılmıyor — bekleme grubu (sync.WaitGroup) her işçinin
grup.Done() çağırdığı ana kadar Wait()i bloke ediyor, ve sekiz işçinin sekizi de kendi
raporunu göndermeden grup.Done()e ulaşamıyor (yardımcı işlevin sonundaki satır sırası buna
bağlı). Bu sıralama olmasaydı, ana goroutine henüz hiçbir rapor ulaşmadan raporlar kanalını
kapatabilir, ve for r := range raporlar döngüsü sekiz yerine daha az rapor görebilirdi —
koşumdan koşuma değişen bir sayı. grup.Wait()in burada yaptığı iş tam olarak bu: sekiz
işçinin hepsinin işini bitirdiğini, herhangi bir süre beklemeden, kanal üzerinden
garanti altına almak.
Kanalların tamponlu açılması da rastgele değil. isler tam isSayisi kapasitesiyle açılıyor,
böylece ana goroutine bütün işleri hiçbir işçi henüz başlamamışken bile gönderebiliyor —
gönderim işçilerin okumaya başlamasını beklemiyor. raporlar da isciSayisi kapasitesiyle
açılıyor, çünkü sekiz işçinin sekizi de raporunu göndermek isteyecek ve tamponsuz bir kanalda
bu gönderim ana goroutine’in okumasını beklerdi — beklerken de bir işçi rapor göndermeye
çalışırken diğerleri henüz grup.Done()e ulaşmamış olabilirdi, sıralamayı gereksiz yere
karmaşıklaştırırdı. Tamponlar burada bir hız optimizasyonu değil, kodun akışını basit
tutan bir tasarım kararı.
KL1. Burada bilinçli bir seçim var: ölçüm hangi işçinin hangi işi aldığını hiç yazdırmıyor. Sekiz işçi aynı kanaldan okumak için yarışıyor ve bu yarışın kazananı çalıştırmadan önce bilinemez — bir koşumda üçüncü işçi iki iş alırken bir başka koşumda hiç almayabilir. Toplam alınan iş (beş işçinin verdiği raporların toplamı) ve toplam rapor eden işçi sayısı (sekiz) ise hiçbir zaman değişmiyor: birincisi kaç iş gönderildiğinin, ikincisi kaç işçi başlatıldığının doğrudan sonucu, hangi işçinin ne aldığından bağımsız. Ders yalnız değişmeyeni ölçüyor.
KL2. “Hepsi aynı sebeple mi durdu” sorusunun yanıtı da aynı nedenle sabit: hangi işçi kaç
iş alırsa alsın, isler kanalı kapandığında her işçinin for is := range isler döngüsü
biter ve rapor gönderilir. Durma sebebi iş dağılımından bağımsız; yalnız durma anı
işçiden işçiye değişiyor, ve bu ders zaten anı değil sebebi ölçüyor.
KL3. Ölçümün kendi başına neyi yazdırmadığı da kadar önemli: hiçbir satır “işçi 3, 2 iş
aldı” demiyor. Sebebi, bu bilginin bir koşumdan diğerine aynı kalmayacak olması — isler
kanalını sekiz goroutine aynı anda dinliyor ve hangisinin hangi göndermeyi yakalayacağı Go’nun
çalışma zamanı zamanlayıcısına bağlı. Bu bir kusur değil, paylaşılan bir kanalın doğal sonucu;
ama kod_dogrula.py bir koşumun çıktısını bir öncekiyle birebir karşılaştırıyor, bu yüzden bu
düzeydeki bir ayrıntıyı yazdırmak dersi kendi doğrulama aracına karşı kırardı. Ölçüm bunun
yerine yalnız koşumdan koşuma değişmeyen iki toplamı (alınan iş, rapor eden işçi) yazdırıp
üçüncü sayıyı (boş kalan işçi) bunlardan türetiyor — gözlemlenen değil hesaplanan bir sayı.
isci işlevinin aldığı id parametresi bu yüzden gövdede hiç okunmuyor: kimliğin kendisi
değil, kaç işçinin var olduğu ve hangi sınıfta durduğu ölçülüyor.
İşçi Sayısını Artırmak İşi Bölmez
KL4. Son satır kursun en net sınırlayıcı gözlemlerinden biri: sekiz işçi başlatıldı ama
yalnız beş iş vardı. Beş işçinin verdiği raporların toplamı beşe eşit olduğuna göre — her
rapor en az sıfır iş taşıyor ve toplam beş — en fazla beş işçi bir iş almış olabilir; kalan
en az üç işçi hiç iş almadan durmuş olmalı. Bu sonuç iki doğrudan ölçülen sayıdan (toplam
alınan iş, toplam işçi sayısı) çıkan bir aritmetik zorunluluk; hangi üç işçinin boş
kaldığını gözlemlemeye hiç gerek yok, çünkü kaçının boş kaldığı zaten toplamlardan belli. Aynı
mantık her isSayisi < isciSayisi durumunda geçerli: işçi sayısı sekizden dokuza çıkarılsaydı,
boş kalan işçi sayısının alt sınırı da üçten dörde çıkardı — işçi eklemek iş miktarını
değiştirmediği sürece, boş kalan işçi sayısının alt sınırı işçi sayısıyla birlikte büyür,
onunla ters orantılı değil.
Tersi durum da aynı aritmetikten çıkıyor: isSayisi isciSayisiden büyük olsaydı (örneğin
yirmi iş, sekiz işçi), boş kalan işçi sayısının alt sınırı sıfıra iner — sekiz işçinin sekizi
de en az bir iş almak zorunda kalır, çünkü yirmi işin hepsi bir yerde işlenmeli ve işleyecek
sekiz kanal ucu var. Boş kalma yalnız iş sayısı işçi sayısından az olduğunda ortaya çıkan
bir durum; havuzun kendisi bu iki yönü de aynı kod yoluyla, hiçbir özel durum eklemeden
karşılıyor.
İşçi sayısını artırmanın var olan işi bölmediği buradan görülüyor: iş kanalındaki beş iş,
kaç işçi dinlerse dinlesin, yalnız beş kez alınabiliyor. Sekizinci işçiyi eklemek beşinci işin
daha küçük parçalara ayrılmasını sağlamıyor — yalnız daha fazla işçinin sırada beklemesine,
ve kanal kapandığında elleri boş durmasına yol açıyor. Bir işin kendisi bölünebilir bir birim
değilse (yukarıdaki is * is gibi, tek bir tam sayıyı işleyen atomik bir adım), işçi sayısını
iş sayısının üstüne çıkarmanın getirisi paralellik değil, yalnız boşta bekleyen işçi. Gerçek
bir sistemde bu genellikle zararsızdır — boşta bekleyen bir goroutine kaynak tüketmez, yalnız
kanalı dinler — ama havuzun büyüklüğünü iş yüküne göre ayarlamayı planlayan bir tasarımcı için
bu ölçüm şunu söylüyor: işçi sayısını artırmak, işin kendisi daha küçük parçalara bölünebilir
olmadıkça, hızlanmayı garanti etmiyor. Bu havuz düzeni burada yürütücü kavramıyla
karşılaştırılmıyor — M07/K04 Eşzamanlılık ve Başarım‘ın havuz karşılaştırması burada
tekrarlanmıyor; ölçülen yalnız Go’nun kendi kanal tabanlı düzeneği.
Havuzun büyüklüğünü belirlemek bu yüzden işin kendi yapısına bakmayı gerektiriyor: iş tek bir tam sayıyı karelemek gibi bölünemez bir adımsa, işçi sayısı iş sayısını geçtiği an fayda düzleşiyor. İş daha büyük bir kümeyi (örneğin bir dilimin elemanlarını) taşıyorsa, aynı iş kanalına o kümenin alt parçalarını ayrı ayrı göndermek işçi sayısını yeniden anlamlı kılıyor — ama bu, işin kendisinin nasıl parçalandığına dair bir karar, havuzun kendisinin çözdüğü bir şey değil.
Kapatılmayan Kuyruk: İşçiler Hiç Durmaz
Yukarıdaki ölçüm isler kanalını close(isler) ile kapatarak hepsinin durmasını sağladı.
Bu kapatma satırı unutulsaydı ne olurdu? Aşağıdaki ölçüm aynı düzeni kuruyor ama iş kanalını
hiç kapatmıyor.
// sizinti.go — is kanali hic kapatilmazsa isciler hic durmaz; sizinti boolean olarak olculur
package main
import (
"fmt"
"runtime"
)
func isciSizinti(id int, isler <-chan int, sonuclar chan<- int) {
for is := range isler {
sonuclar <- is * is
}
// Bu satira hicbir zaman ulasilmiyor: isler kanali hic kapanmiyor.
}
func main() {
taban := runtime.NumGoroutine()
const isciSayisi = 4
isler := make(chan int) // kapatilmiyor
sonuclar := make(chan int, isciSayisi)
for id := 1; id <= isciSayisi; id++ {
go isciSizinti(id, isler, sonuclar)
}
fmt.Println("-- is kanali hic kapatilmadi --")
fmt.Printf("baslatilan isci sayisi: %d\n", isciSayisi)
fmt.Printf("durmayan isci kaldi mi: %v\n", runtime.NumGoroutine() > taban)
}
-- is kanali hic kapatilmadi -- baslatilan isci sayisi: 4 durmayan isci kaldi mi: true
KL5. Dört işçi başlatıldı ve hiçbiri hiç bir iş almadı — çünkü isler kanalına hiçbir
değer gönderilmedi. Ama asıl gözlem bu değil: dört işçinin dördü de for is := range isler
satırında sonsuza kadar bekliyor, çünkü kanal ne bir değer taşıyor ne kapanıyor. Bu, önceki
derste kurulan “kendi durdu da bir sözleşmedir” gözleminin havuz düzenindeki karşılığı: bir
işçinin durması, kendi gövdesinin bir kararı değil, iş kanalını yöneten tarafın bir kararı.
runtime.NumGoroutine()’ın taban sayının üstünde kalması bunun tek kanıtı — program hiçbir
hata, hiçbir uyarı vermeden normal çıkışla sonlanıyor, dört goroutine arkada asılı kalmış
olsa bile. Kaç goroutine kaldığı burada basılmıyor, yalnız kaldığının kendisi.
Bu ölçümün doğruluğu koşum zamanlamasına bağlı değil: go deyimi çalıştığı anda yeni goroutine
çalışma zamanına kaydediliyor, henüz hiçbir kod çalıştırmamış olsa bile; bu yüzden döngü
bitip runtime.NumGoroutine() çağrıldığında dört işçinin dördü de sayıma zaten girmiş oluyor.
Ölçüm bu yüzden dört işçinin fiilen bloklanmasını beklemeye gerek duymuyor — kaydın kendisi
yeterli, çünkü mesele işçilerin şu an ne yaptığı değil, var olup olmadıkları.
Bu tek sızıntı kısa ömürlü bir program için gözlemlenebilir bir soruna yol açmayabilir —
dört goroutine, sonlanan bir sürecin belleğiyle birlikte zaten temizlenir. Asıl risk deseni
tekrarlamakta: bir istek işleyicisi her çağrıldığında kendi havuzunu kurup iş kanalını
kapatmayı unutuyorsa, sızan işçi sayısı gelen isteklerle birlikte büyür. Bu ders tek bir
sızıntıyı ölçüyor, çoğaltılmış bir sızıntıyı değil — ama boolean’ın kendisi zaten yeterli
uyarı: true yanıtı, kanalın hiçbir zaman kapanmayacağı bir tasarımın işareti, kaç goroutine
biriktiği önemli değil.
Özet
- Sabit sayıda işçi paylaşılan bir iş kanalından okur; kanal kapandığında hepsi aynı sebeple durur (beklediği geldi), hangi işi kimin aldığından bağımsız olarak.
- Toplam alınan iş ve toplam rapor eden işçi sayısı, iş dağılımından bağımsız iki sabit değerdir; bu ikisinin farkından, en az kaç işçinin hiç iş almadığı hesaplanır.
- İşçi sayısını iş sayısının üstüne çıkarmak var olan işi bölmez; yalnız boş kalan işçi sayısını artırır, çünkü her iş bölünemez bir birim olarak yalnız bir kez alınır.
- İş kanalı kapatılmazsa işçiler sonsuza kadar bekler; sızıntının kanıtı yalnız bir boolean olarak ölçülür, kaç goroutine kaldığı basılmaz.
- Kapatma sorumluluğu burada da gönderende durur — işçi havuzunda gönderen, işi dağıtan taraftır.
Sonraki Adım
Bu derste bütün işçiler aynı kanaldan okudu ve kanalın kapanması hepsine aynı anda ulaştı. Ama her zaman tek bir kaynak yeterli olmuyor: bazen bir iş, tek bir üreticinin üretebileceğinden daha hızlı tüketilmesi gereken bir akıştan geliyor, ve o akışın kendisi birden fazla tüketiciye yayılmak zorunda. Sıradaki ders bu yayılmayı ölçüyor — bir kaynağın çok sayıda tüketiciye dağılması ve sonuçların tek bir kanalda yeniden toplanması — ve bu sefer sorulan soru işçi havuzundakinden farklı: toplama kanalını kapatma sorumluluğu birden fazla üreticiden hangisinde duruyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.