Paket sync menyediakan primitif sinkronisasi untuk mengoordinasikan goroutine, yaitu fungsi yang dijalankan serentak oleh runtime Go. Topik ini membahas sync.WaitGroup, struktur penghitung yang menunggu sekelompok goroutine menuntaskan pekerjaannya sebelum program berlanjut. Pembahasan mencakup tiga metodenya, aturan penghitungan yang aman, serta pola pengumpulan hasil yang bebas race condition, yakni kondisi ketika hasil program bergantung pada urutan kejadian yang tidak terkendali. Setelah membacanya, penyelesaian sekelompok goroutine dapat dikoordinasikan tanpa jeda buatan seperti time.Sleep.
Tiga metode: Add, Done, dan Wait#
sync.WaitGroup menghitung jumlah goroutine yang sedang ditunggu melalui tiga metode. Metode Add menaikkan penghitung, metode Done menurunkannya satu angka, dan metode Wait menahan pemanggilnya hingga penghitung kembali ke nol. Pola bakunya tersusun atas tiga langkah, yaitu Add dipanggil sebelum goroutine diluncurkan, Done dijadwalkan dengan defer di awal badan goroutine, dan Wait dipanggil dari goroutine yang menunggu. Program berikut meluncurkan empat pekerja yang masing-masing menjumlahkan kuadrat sepuluh bilangan, lalu mencetak rekapnya setelah semua pekerja tuntas.
package main
import (
"fmt"
"sync"
)
// hitungKuadrat menjumlahkan kuadrat bilangan dari awal sampai akhir,
// lalu menyimpan hasilnya pada slot milik pekerja itu sendiri.
func hitungKuadrat(wg *sync.WaitGroup, id, awal, akhir int, hasil []int) {
defer wg.Done()
jumlah := 0
for n := awal; n <= akhir; n++ {
jumlah += n * n
}
hasil[id] = jumlah
}
func main() {
var wg sync.WaitGroup
hasil := make([]int, 4)
for id := 0; id < 4; id++ {
wg.Add(1)
go hitungKuadrat(&wg, id, id*10+1, id*10+10, hasil)
}
wg.Wait()
for id, jumlah := range hasil {
fmt.Printf("pekerja %d: jumlah kuadrat %d\n", id, jumlah)
}
}pekerja 0: jumlah kuadrat 385
pekerja 1: jumlah kuadrat 2485
pekerja 2: jumlah kuadrat 6585
pekerja 3: jumlah kuadrat 12685Urutan baris keluaran selalu sama meskipun empat goroutine berjalan bersamaan. Setiap pekerja menulis hasilnya pada slot yang berbeda di slice hasil, sehingga tidak ada dua goroutine yang menyentuh sel yang sama. Pencetakan dilakukan sekali oleh fungsi main setelah Wait kembali, sehingga tidak ada persaingan urutan cetak antargoroutine. Bila pencetakan dilakukan dari dalam goroutine, urutan barisnya berubah-ubah antareksekusi dan keluaran tidak dapat dipercaya.
Zero value dari sync.WaitGroup sudah siap dipakai tanpa inisialisasi, sehingga deklarasi var wg sync.WaitGroup pada program di atas langsung berfungsi. Pemanggilan Wait pada penghitung yang memang nol sejak awal langsung kembali tanpa menunggu. Penempatan Add di dalam goroutine menjadi berbahaya karena Wait dapat tercapai sebelum Add dijalankan. Satu panggilan Add juga dipasangkan dengan tepat satu panggilan Done, dan ketidakseimbangan keduanya adalah sumber macet yang paling sering ditemukan.
Program yang sama dijalankan dari folder latihan tetap di /tmp dengan urutan berikut.
mkdir -p /tmp/go-waitgroup
cp rekap-kuadrat.go /tmp/go-waitgroup/
cd /tmp/go-waitgroup
go run rekap-kuadrat.gopekerja 0: jumlah kuadrat 385
pekerja 1: jumlah kuadrat 2485
pekerja 2: jumlah kuadrat 6585
pekerja 3: jumlah kuadrat 12685Penempatan Add yang aman#
Penempatan Add menentukan kebenaran seluruh pola. Cara yang paling aman adalah memanggil Add sebelum pernyataan go, sebagaimana contoh di atas, karena penghitung sudah terisi ketika Wait mulai berjalan. Pemanggilan Add juga boleh dilakukan sekali untuk banyak goroutine, misalnya wg.Add(4) sebelum perulangan peluncuran, dan hasilnya sama selama pemanggilannya mendahului Wait. Jumlah pada Add perlu disesuaikan dengan jumlah goroutine yang diluncurkan, karena kelebihan angka membuat Wait menunggu Done yang tidak pernah datang. Penempatan Add di dalam goroutine justru membuka celah berikut.
// tidak-dijalankan
var wg sync.WaitGroup
for id := 0; id < 4; id++ {
go func() {
wg.Add(1) // berbahaya: Wait bisa melihat penghitung masih 0
defer wg.Done()
proses(id)
}()
}
wg.Wait()Pada pola tersebut, fungsi main dapat mencapai Wait sebelum satu pun goroutine sempat memanggil Add. Penghitung masih bernilai nol pada saat itu, sehingga Wait langsung kembali dan program berlanjut tanpa menunggu pekerja mana pun. Kesalahan semacam ini tidak selalu terlihat pada uji cepat karena hanya muncul ketika penjadwal memang berhenti di titik yang sial. Konsistensi menempatkan Add sebelum peluncuran menutup celah itu sepenuhnya.
Penerusan alamat, bukan salinan#
WaitGroup tidak boleh disalin setelah digunakan. Struktur ini memuat penghitung internal yang memiliki identitas, sehingga seluruh goroutine yang terlibat harus menerima alamat yang sama. Pada program di atas, parameter hitungKuadrat bertipe *sync.WaitGroup dan pemanggilannya menyertakan &wg. Menyalin nilai WaitGroup ke variabel lain atau meneruskannya tanpa & menciptakan dua penghitung berbeda yang tidak saling terhubung. Akibatnya Wait menunggu penghitung yang tidak pernah dikurangi dan program macet dengan pesan deadlock, padahal pekerjanya sudah selesai.
Pola yang lazim untuk pekerja yang lebih besar adalah menyematkan sync.WaitGroup sebagai field struct dan meneruskan alamat struct itu kepada setiap goroutine. Bentuk ini menjaga satu penghitung untuk seluruh kelompok pekerja sekaligus memudahkan pemeriksaan alat statis. Struktur yang memuat WaitGroup, Mutex, dan sejenisnya memang dilarang disalin setelah dipakai, dan aturan yang sama berlaku bagi struct pembungkusnya.
Kesalahan umum#
- Melupakan
Done. Penghitung tidak pernah mencapai nol sehinggaWaitmenahan program selamanya dengan pesanall goroutines are asleep - deadlock!. Perbaikannya, jadwalkanwg.Done()dengandefersebagai baris pertama badan goroutine agar tetap terpanggil pada semua jalur keluar. - Menggantikan
Waitdengantime.Sleep. Jeda buatan hanya menebak lamanya pekerjaan, sehingga program bisa berlanjut sebelum pekerja selesai atau menganggur lebih lama dari perlu. Perbaikannya, gunakanWaityang menyesuaikan diri dengan keadaan nyata. - Meneruskan
WaitGroupsebagai nilai. Setiap salinan membawa penghitung sendiri dan tidak ada koordinasi yang terjadi. Perbaikannya, teruskan*sync.WaitGroupkepada seluruh goroutine yang terlibat. - Mengisi angka berlebih pada
Add. Penghitung tidak pernah mencapai nol karenaDonedipanggil lebih sedikit daripada nilai yang ditambahkan. Perbaikannya, hitung jumlah goroutine yang benar-benar diluncurkan sebelum memanggilAdd.
Ringkasan#
sync.WaitGroupmenghitung goroutine yang ditunggu melaluiAdd,Done, danWait.Adddipanggil sebelum pernyataango, sedangkanDonedijadwalkan dengandeferdi awal badan goroutine.- Hasil pekerjaan dikumpulkan pada slot yang berbeda lalu dicetak setelah
Waitkembali agar keluaran tetap terurut. WaitGroupditeruskan sebagai pointer dan tidak boleh disalin setelah digunakan.
Referensi#
- sync — Go Documentation
— Dokumentasi resmi
sync.WaitGroupbeserta syarat pemakaiannya. - The Go Memory Model — The Go Project
— Menjamin hubungan sebab akibat antara
Donedan kembalinyaWait. - Effective Go — The Go Project — Panduan resmi gaya Go yang memperlihatkan peluncuran goroutine dan koordinasi hasilnya.