Goroutine memang murah, tetapi “murah” bukan berarti gratis. Bayangkan satu juta tugas yang masing-masing perlu memanggil API luar: membuat satu goroutine per tugas berarti satu juta goroutine hidup bersamaan, memakan memori untuk tumpukan masing-masing, dan saling berebut waktu prosesor — sementara API tujuan pasti menolak banjir permintaan itu. Pola worker pool menyelesaikan masalahnya dengan membalik arah: jumlah pekerja dibuat tetap (misalnya tiga), tugas mengalir masuk lewat sebuah channel, dan setiap pekerja mengambil tugas satu per satu sekuat kemampuannya. Chapter ini membongkar pola tersebut sampai detail penutupan channel dan cara menjaga output tetap deterministik.

Anatomi Pola #

Worker pool tersusun dari tiga komponen yang saling terhubung channel:

Pertama, channel tugas menjadi antrean kerja. Kedua, sejumlah goroutine pekerja menjalankan perulangan for range di atas channel tugas — blok perulangan berakhir secara alami saat channel ditutup, jadi pekerja tidak perlu diberi tahu secara khusus. Ketiga, channel hasil menampung keluaran para pekerja untuk dikumpulkan pemanggil. Jumlah tugas boleh jutaan; jumlah goroutine tetap sekecil yang kita tentukan.

Implementasi Lengkap #

Berkas workerpool-demo.go memproses lima tugas dengan tiga pekerja. Setiap pekerja mensimulasikan kerja selama 10 milidetik, hasil dikumpulkan lalu diurutkan sebelum dicetak:

go
package main

import (
	"fmt"
	"slices"
	"sync"
	"time"
)

type Hasil struct {
	ID      int
	Kuadrat int
}

func main() {
	const jumlahPekerja = 3
	daftarTugas := []int{1, 2, 3, 4, 5}

	tugas := make(chan int)
	// Buffer seukuran jumlah tugas: pekerja bisa menulis hasil
	// meskipun main masih sibuk mengirim tugas (menghindari deadlock).
	hasil := make(chan Hasil, len(daftarTugas))

	var wg sync.WaitGroup
	for pekerja := 1; pekerja <= jumlahPekerja; pekerja++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for id := range tugas {
				time.Sleep(10 * time.Millisecond) // simulasi kerja
				hasil <- Hasil{ID: id, Kuadrat: id * id}
			}
		}()
	}

	// Sebarkan semua tugas lalu tutup channel tugas agar pekerja berhenti.
	for _, id := range daftarTugas {
		tugas <- id
	}
	close(tugas)

	// Tutup channel hasil setelah semua pekerja selesai.
	go func() {
		wg.Wait()
		close(hasil)
	}()

	// Kumpulkan hasil, lalu urutkan berdasarkan ID agar output deterministik.
	terkumpul := []Hasil{}
	for h := range hasil {
		terkumpul = append(terkumpul, h)
	}
	slices.SortFunc(terkumpul, func(a, b Hasil) int {
		return a.ID - b.ID
	})

	fmt.Println("hasil pekerjaan (terurut berdasarkan ID):")
	for _, h := range terkumpul {
		fmt.Printf("tugas %d → kuadrat %d\n", h.ID, h.Kuadrat)
	}
}

Jalankan program tersebut dari direktori chapter:

bash
go run workerpool-demo.go

Output yang Dihasilkan:

text
hasil pekerjaan (terurut berdasarkan ID):
tugas 1 → kuadrat 1
tugas 2 → kuadrat 4
tugas 3 → kuadrat 9
tugas 4 → kuadrat 16
tugas 5 → kuadrat 25

Mari kita telaah baris demi baris program di atas:

  1. Baris 10–13 (type Hasil): struct hasil yang membawa ID tugas dan nilainya, sehingga hasil bisa diurutkan kembali setelah dikumpulkan.
  2. Baris 19–22 (dua channel): channel tugas tanpa buffer sebagai pertemuan langsung pengirim dan pekerja; channel hasil berbuffer seukuran jumlah tugas.
  3. Baris 25–34 (pekerja): tiga goroutine menjalankan for id := range tugas; perulangan berhenti otomatis saat tugas ditutup; time.Sleep 10 milidetik mensimulasikan kerja nyata.
  4. Baris 37–40 (sebar dan tutup): main mengirim lima tugas lalu memanggil close(tugas) — penutupan channel adalah sinyal “tidak ada tugas lagi” bagi semua pekerja.
  5. Baris 43–46 (goroutine penutup hasil): sebuah goroutine kecil menunggu wg.Wait() lalu menutup hasil; tanpa ini, perulangan pengumpulan di main tidak akan pernah berakhir.
  6. Baris 49–55 (kumpulkan dan urutkan): hasil-hasil dikumpulkan ke slice, lalu slices.SortFunc mengurutkan berdasarkan ID — pekerja menyelesaikan tugas dengan urutan yang berbeda-beda tiap eksekusi, dan pengurutan inilah yang membuat output selalu sama.
  7. Baris 57–60: mencetak hasil terurut, satu baris per tugas.

Menentukan Jumlah Pekerja #

Angka pekerja ideal bergantung pada jenis pekerjaannya. Untuk pekerjaan yang menghabiskan prosesor — kompresi, enkripsi, perhitungan — jumlah pekerja yang mendekati runtime.GOMAXPROCS(0) biasanya paling efisien, sebab lebih dari itu hanya menambah perpindahan konteks. Untuk pekerjaan yang banyak menunggu I/O — panggilan HTTP, query database — jumlah pekerja bisa jauh lebih besar daripada jumlah prosesor karena goroutine menunggu tidak memakan prosesor. Ukur dengan benchmark atau uji beban sebelum mematok angka; tebakan awal yang wajar adalah GOMAXPROCS untuk beban CPU dan kelipatannya untuk beban I/O.

Penerapan Lebih Besar #

Pola pada contoh ini adalah kerangka minimal. Pada proyek nyata biasanya ditambah context untuk pembatalan, batas antrean, penanganan galat per tugas, dan pelaporan kemajuan. Proyek Concurrent Worker Pool pada course ini merangkai semuanya menjadi program utuh yang bisa Anda bandingkan dengan contoh di sini.

Dengan worker pool, banyak pekerjaan berjalan dalam jumlah goroutine yang terkendali. Bab berikutnya menampilkan pola pemanfaan channel yang berbeda arah: memecah proses menjadi tahapan yang mengalir — pipeline.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

workerpool-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"slices"
	"sync"
	"time"
)

type Hasil struct {
	ID      int
	Kuadrat int
}

func main() {
	const jumlahPekerja = 3
	daftarTugas := []int{1, 2, 3, 4, 5}

	tugas := make(chan int)
	// Buffer seukuran jumlah tugas: pekerja bisa menulis hasil
	// meskipun main masih sibuk mengirim tugas (menghindari deadlock).
	hasil := make(chan Hasil, len(daftarTugas))

	var wg sync.WaitGroup
	for pekerja := 1; pekerja <= jumlahPekerja; pekerja++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for id := range tugas {
				time.Sleep(10 * time.Millisecond) // simulasi kerja
				hasil <- Hasil{ID: id, Kuadrat: id * id}
			}
		}()
	}

	// Sebarkan semua tugas lalu tutup channel tugas agar pekerja berhenti.
	for _, id := range daftarTugas {
		tugas <- id
	}
	close(tugas)

	// Tutup channel hasil setelah semua pekerja selesai.
	go func() {
		wg.Wait()
		close(hasil)
	}()

	// Kumpulkan hasil, lalu urutkan berdasarkan ID agar output deterministik.
	terkumpul := []Hasil{}
	for h := range hasil {
		terkumpul = append(terkumpul, h)
	}
	slices.SortFunc(terkumpul, func(a, b Hasil) int {
		return a.ID - b.ID
	})

	fmt.Println("hasil pekerjaan (terurut berdasarkan ID):")
	for _, h := range terkumpul {
		fmt.Printf("tugas %d → kuadrat %d\n", h.ID, h.Kuadrat)
	}
}