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:
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:
go run workerpool-demo.goOutput yang Dihasilkan:
hasil pekerjaan (terurut berdasarkan ID):
tugas 1 → kuadrat 1
tugas 2 → kuadrat 4
tugas 3 → kuadrat 9
tugas 4 → kuadrat 16
tugas 5 → kuadrat 25Mari kita telaah baris demi baris program di atas:
- Baris 10–13 (
type Hasil): struct hasil yang membawa ID tugas dan nilainya, sehingga hasil bisa diurutkan kembali setelah dikumpulkan. - Baris 19–22 (dua channel): channel
tugastanpa buffer sebagai pertemuan langsung pengirim dan pekerja; channelhasilberbuffer seukuran jumlah tugas. - Baris 25–34 (pekerja): tiga goroutine menjalankan
for id := range tugas; perulangan berhenti otomatis saattugasditutup;time.Sleep10 milidetik mensimulasikan kerja nyata. - Baris 37–40 (sebar dan tutup):
mainmengirim lima tugas lalu memanggilclose(tugas)— penutupan channel adalah sinyal “tidak ada tugas lagi” bagi semua pekerja. - Baris 43–46 (goroutine penutup hasil): sebuah goroutine kecil menunggu
wg.Wait()lalu menutuphasil; tanpa ini, perulangan pengumpulan dimaintidak akan pernah berakhir. - Baris 49–55 (kumpulkan dan urutkan): hasil-hasil dikumpulkan ke slice, lalu
slices.SortFuncmengurutkan berdasarkan ID — pekerja menyelesaikan tugas dengan urutan yang berbeda-beda tiap eksekusi, dan pengurutan inilah yang membuat output selalu sama. - 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 #
- Effective Go — Concurrency — pola berbagi pekerjaan antar goroutine.
- Package sync
—
WaitGroupdan alat koordinasi pendukung worker pool.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
workerpool-demo.go
Unduh berkaspackage 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)
}
}