İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat