Goroutine murah — jauh lebih murah daripada thread OS — tetapi gratis hanya sampai batas tertentu. Ribuan goroutine yang berebut satu variabel tanpa sinkronisasi bisa jadi data race; ribuan worker untuk pekerjaan CPU-bound justru saling berebut core dan menambah biaya penjadwalan. Chapter ini merakit worker pool dengan proporsi yang tepat, menjaga hasil tetap terurut tanpa mutex, lalu membahas tiga perangkap klasik concurrency: contention, kerja sia-sia, dan data race.
Fan-out: Membagi Kerja ke Worker #
Berkas conc-demo.go membandingkan eksekusi berurutan dengan worker pool. Penyalur mengirim nomor tugas ke satu channel, sejumlah runtime.NumCPU() worker mengambil tugas dari channel yang sama, dan hasil ditulis ke indeks milik tugasnya masing-masing.
package main
import (
"fmt"
"runtime"
"slices"
"sync"
)
// hitungTugas mensimulasikan kerja CPU-bound murni dengan hasil tetap,
// sehingga output program tetap deterministik meski dieksekusi paralel.
func hitungTugas(n int) int {
total := 0
for i := 1; i <= 10_000; i++ {
total += i * (n + 1)
}
return total
}
func main() {
jumlahTugas := 8
// Pembanding: eksekusi berurutan, satu tugas selesai baru tugas berikutnya.
urutan := make([]int, jumlahTugas)
for i := range jumlahTugas {
urutan[i] = hitungTugas(i)
}
// Worker pool: sejumlah runtime.NumCPU() goroutine mengambil tugas
// dari channel yang sama, hasil ditulis ke indeks masing-masing.
hasil := make([]int, jumlahTugas)
tugas := make(chan int)
var wg sync.WaitGroup
jumlahWorker := runtime.NumCPU()
for w := 0; w < jumlahWorker; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := range tugas {
hasil[n] = hitungTugas(n) // tiap indeks hanya ditulis satu worker
}
}()
}
// Penyalur tugas: kirim semua indeks lalu tutup channel agar worker
// keluar dari loop range.
for i := range jumlahTugas {
tugas <- i
}
close(tugas)
wg.Wait()
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
fmt.Println("Worker dibuat:", jumlahWorker, "(setara runtime.NumCPU)")
fmt.Println("Hasil paralel identik dengan berurutan:", slices.Equal(hasil, urutan))
fmt.Println("Hasil terurut berdasar nomor tugas:", hasil)
}Jalankan dari folder chapter:
go run conc-demo.goOutput yang Dihasilkan:
GOMAXPROCS: 2
Worker dibuat: 2 (setara runtime.NumCPU)
Hasil paralel identik dengan berurutan: true
Hasil terurut berdasar nomor tugas: [50005000 100010000 150015000 200020000 250025000 300030000 350035000 400040000]Mari kita telaah baris demi baris program di atas:
- Baris 12-18 (
func hitungTugas): pekerjaan CPU-bound murni dengan hasil tetap — total50005000 × (n+1)untuk tiap nomor tugas, identik di semua eksekusi. - Baris 24-27 (pembanding berurutan): hasil eksekusi berurutan disimpan sebagai pembanding kebenaran.
- Baris 35-44 (worker pool): tiap worker berisi loop
for n := range tugasyang terus mengambil tugas — goroutine dipakai ulang, bukan satu goroutine per tugas. - Baris 41 (
hasil[n] = ...): tiap indeks hanya ditulis oleh satu worker, sehingga tidak perlu mutex dan hasil tetap terurut berdasar nomor tugas. - Baris 48-52: penyalur kirim semua indeks lalu
close(tugas); looprangeworker berakhir saat channel tertutup, danwg.Wait()menunggu semuanya selesai.
CPU-bound atau IO-bound #
Jumlah worker ideal mengikuti jenis pekerjaan. Pekerjaan CPU-bound — kompresi, hash, format teks — sudah jenuh ketika worker sama banyak dengan core; worker tambahan hanya menambah konteks peralihan. Pekerjaan IO-bound — panggilan HTTP, query database, baca berkas — menghabiskan sebagian besar waktu menunggu, sehingga worker bisa jauh lebih banyak daripada core (misalnya puluhan hingga ratusan) tanpa membuat CPU sibuk. Batas atas praktisnya adalah memori dan target penggunaan berkas/koneksi.
| Jenis kerja | Jumlah worker awal yang wajar |
|---|---|
| CPU-bound | runtime.NumCPU() |
| IO-bound cepat (cache lokal) | 2-4 × NumCPU |
| IO-bound lambat (jaringan) | puluhan, dibatasi target beban lawan |
Hasil Terurut Tanpa Mutex #
Kunci pola pada program di atas adalah tiap slot keluaran hanya dimiliki satu worker. hasil[n] = hitungTugas(n) tidak butuh mutex karena tidak ada dua goroutine yang menulis indeks sama; hasil pun terurut karena indeksnya tetap. Ketika pola ini tidak memungkinkan dan banyak goroutine menulis satu variabel, contention pada mutex muncul: goroutine antre pada lock dan sebagian core menganggur. Solusi klasiknya sharding — membagi satu map besar ke N map kecil, masing-masing dengan lock sendiri, lalu menggabungkan di akhir — sehingga goroutine tersebar dan antrean lock memendek.
context Membatalkan Kerja Sia-sia #
Kerja yang hasilnya sudah tidak dibutuhkan tetap memakan CPU sampai selesai. context.WithCancel (atau WithTimeout) memberi sinyal pembatalan: worker memeriksa ctx.Done() di antara tugas dan berhenti lebih awal. Pola lengkapnya dibahas di chapter Cancellation dan Timeout
; untuk performa, intinya satu: panggilan yang dibatalkan lebih murah daripada panggilan yang selesai sia-sia.
errgroup dan Batas Goroutine #
Paket golang.org/x/sync menyediakan errgroup.Group yang menggabungkan WaitGroup dengan propagasi error, plus varian SetLimit(n) yang membatasi jumlah goroutine aktif sekaligus — pola yang lebih ringkas daripada channel buffered sebagai semaphore untuk membatasi fan-out. Untuk kontrol alur dan error lintas goroutine, errgroup layak jadi pilihan standar; untuk pola channel seperti program di atas, standard library sudah cukup.
Dengan fan-out yang proporsional dan hasil yang aman dari race, satu variabel runtime tersisa yang menentukan kecepatan keseluruhan: seberapa sering garbage collector berjalan — topik penalaan GOGC dan GOMEMLIMIT di chapter berikutnya.
Referensi #
- Package sync
—
WaitGroup,Mutex, danOnce. - Package runtime — GOMAXPROCS — batas thread yang mengeksekusi goroutine.
- Blog Go: Race Detector
— cara kerja dan pemakaian
-race.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
conc-demo.go
Unduh berkaspackage main
import (
"fmt"
"runtime"
"slices"
"sync"
)
// hitungTugas mensimulasikan kerja CPU-bound murni dengan hasil tetap,
// sehingga output program tetap deterministik meski dieksekusi paralel.
func hitungTugas(n int) int {
total := 0
for i := 1; i <= 10_000; i++ {
total += i * (n + 1)
}
return total
}
func main() {
jumlahTugas := 8
// Pembanding: eksekusi berurutan, satu tugas selesai baru tugas berikutnya.
urutan := make([]int, jumlahTugas)
for i := range jumlahTugas {
urutan[i] = hitungTugas(i)
}
// Worker pool: sejumlah runtime.NumCPU() goroutine mengambil tugas
// dari channel yang sama, hasil ditulis ke indeks masing-masing.
hasil := make([]int, jumlahTugas)
tugas := make(chan int)
var wg sync.WaitGroup
jumlahWorker := runtime.NumCPU()
for w := 0; w < jumlahWorker; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := range tugas {
hasil[n] = hitungTugas(n) // tiap indeks hanya ditulis satu worker
}
}()
}
// Penyalur tugas: kirim semua indeks lalu tutup channel agar worker
// keluar dari loop range.
for i := range jumlahTugas {
tugas <- i
}
close(tugas)
wg.Wait()
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
fmt.Println("Worker dibuat:", jumlahWorker, "(setara runtime.NumCPU)")
fmt.Println("Hasil paralel identik dengan berurutan:", slices.Equal(hasil, urutan))
fmt.Println("Hasil terurut berdasar nomor tugas:", hasil)
}