---
title: 'İşçi Havuzları'
source: 'https://academia.sh/tr/kurslar/go-eszamanlilik/isci-havuzlari'
course: Eşzamanlılık
language: tr
updated: '2026-08-23T16:55:14+00:00'
license: 'CC BY-SA 4.0'
---

# İşç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.

Ö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.

```go
// 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` `isciSayisi`den 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.

```go
// 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?
