sync.WaitGroup

Mengoordinasikan penyelesaian goroutine.

Daftar isi

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.

go rekap-kuadrat.go
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)
	}
}
text Keluaran
pekerja 0: jumlah kuadrat 385
pekerja 1: jumlah kuadrat 2485
pekerja 2: jumlah kuadrat 6585
pekerja 3: jumlah kuadrat 12685

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

bash
mkdir -p /tmp/go-waitgroup
cp rekap-kuadrat.go /tmp/go-waitgroup/
cd /tmp/go-waitgroup
go run rekap-kuadrat.go
text Keluaran
pekerja 0: jumlah kuadrat 385
pekerja 1: jumlah kuadrat 2485
pekerja 2: jumlah kuadrat 6585
pekerja 3: jumlah kuadrat 12685

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

go Penempatan Add di dalam goroutine yang berisiko
// 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 sehingga Wait menahan program selamanya dengan pesan all goroutines are asleep - deadlock!. Perbaikannya, jadwalkan wg.Done() dengan defer sebagai baris pertama badan goroutine agar tetap terpanggil pada semua jalur keluar.
  • Menggantikan Wait dengan time.Sleep. Jeda buatan hanya menebak lamanya pekerjaan, sehingga program bisa berlanjut sebelum pekerja selesai atau menganggur lebih lama dari perlu. Perbaikannya, gunakan Wait yang menyesuaikan diri dengan keadaan nyata.
  • Meneruskan WaitGroup sebagai nilai. Setiap salinan membawa penghitung sendiri dan tidak ada koordinasi yang terjadi. Perbaikannya, teruskan *sync.WaitGroup kepada seluruh goroutine yang terlibat.
  • Mengisi angka berlebih pada Add. Penghitung tidak pernah mencapai nol karena Done dipanggil lebih sedikit daripada nilai yang ditambahkan. Perbaikannya, hitung jumlah goroutine yang benar-benar diluncurkan sebelum memanggil Add.

Ringkasan#

  • sync.WaitGroup menghitung goroutine yang ditunggu melalui Add, Done, dan Wait.
  • Add dipanggil sebelum pernyataan go, sedangkan Done dijadwalkan dengan defer di awal badan goroutine.
  • Hasil pekerjaan dikumpulkan pada slot yang berbeda lalu dicetak setelah Wait kembali agar keluaran tetap terurut.
  • WaitGroup diteruskan sebagai pointer dan tidak boleh disalin setelah digunakan.

Referensi#