Dua goroutine yang membaca dan menulis memori yang sama tanpa sinkronisasi adalah bencana paling laris di program concurrent — bahayanya justru diam-diam: program berjalan, hasil tampak wajar, kesalahan muncul sesekali di beban tertinggi. Chapter ini membedah data race pada counter bersama, menangkapnya dengan race detector, membahas semantik loop variable sejak Go 1.22, lalu memperbaikinya dengan mutex dan atomic.
Apa Itu Data Race #
Data race terjadi bila tiga syarat terpenuhi sekaligus: dua goroutine mengakses memori yang sama, sedikitnya satu akses adalah penulisan, dan keduanya tidak tersinkronisasi lewat channel, mutex, atau atomic. Operator shared++ tampak tunggal tetapi sebenarnya tiga langkah — baca, tambah, tulis — sehingga dua goroutine bersamaan dapat membaca nilai sama dan saling menimpa hasilnya. Go memory model menyatakan program semacam itu berperilaku tidak terdefinisi.
Program yang Berlomba #
Berkas race-demo.go menghidupkan tiga syarat itu: delapan goroutine masing-masing menambah shared sebanyak 1250 kali — total seharusnya 10000 — tanpa proteksi:
package main
import (
"sync"
)
func main() {
var wg sync.WaitGroup
shared := 0
// Delapan goroutine menambah variabel yang sama tanpa proteksi.
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for range 1250 {
shared++ // data race: baca dan tulis bersamaan
}
}()
}
wg.Wait()
}Program ini sengaja tidak mencetak shared; nilainya berbeda setiap eksekusi — itulah gejalanya — dan kode keluar tetap 0 seolah sehat. Tambahkan dua baris berikut setelah wg.Wait() untuk melihatnya:
// Tambahkan setelah wg.Wait() untuk melihat nilai shared:
fmt.Println("nilai shared:", shared)Lima eksekusi pada mesin kami menghasilkan 7082, 7794, 8174, 7329, dan sekali 10000. Nilai yang kurang adalah penambahan hilang: dua goroutine membaca nilai sama lalu saling menimpa tulisannya. Nilai tepat 10000 pun bukan tanda aman — hanya kebetulan waktu scheduler.
Mendeteksi dengan Race Detector #
Flag -race mengompilasi program dengan perekam bayangan (shadow memory) yang mencatat setiap akses memori dan melaporkan pasangan yang bertentangan. Jalankan berkas yang sama dengan flag itu:
go run -race race-demo.goOutput yang Dihasilkan:
==================
WARNING: DATA RACE
Read at 0x00c000018128 by goroutine 11:
main.main.func1()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:17 +0x99
Previous write at 0x00c000018128 by goroutine 8:
main.main.func1()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:17 +0xab
Goroutine 11 (running) created at:
main.main()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:14 +0x7d
Goroutine 8 (running) created at:
main.main()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:14 +0x7d
==================
==================
WARNING: DATA RACE
Write at 0x00c000018128 by goroutine 10:
main.main.func1()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:17 +0xab
Previous write at 0x00c000018128 by goroutine 11:
main.main.func1()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:17 +0xab
Goroutine 10 (running) created at:
main.main()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:14 +0x7d
Goroutine 11 (running) created at:
main.main()
/home/z/my-project/content/go/concurrency/race-condition/race-demo.go:14 +0x7d
==================
Found 2 data race(s)
exit status 66Anatomi laporan: WARNING: DATA RACE membuka laporan; Read at ... by goroutine 11 menunjukkan akses baca beserta pelakunya; Previous write at ... menunjukkan penulisan sebelumnya pada alamat sama; dua blok created at menunjukkan baris kelahiran goroutine — semuanya menunjuk race-demo.go:17, tepat baris shared++; exit status 66 adalah kode khusus race detector.
Untuk proyek dengan berkas test, jalankan go test -race ./... agar seluruh suite diperiksa — layak dipasang di CI. Detector menaikkan waktu eksekusi dan memori beberapa kali lipat, jadi gunakan saat pengembangan, bukan build produksi.
Semantik Loop Variable Sejak Go 1.22 #
Jebakan klasik lain adalah loop variable. Sebelum Go 1.22, semua iterasi for berbagi satu variabel, sehingga goroutine yang menangkap i sering melihat nilai iterasi terakhir:
// Sebelum Go 1.22: satu variabel i dibagi semua iterasi,
// goroutine di bawah sering melihat nilai 9 (iterasi terakhir).
for i := 0; i < 10; i++ {
go func() { fmt.Print(i) }()
}
// Sejak Go 1.22: setiap iterasi memiliki salinan i sendiri,
// sehingga goroutine melihat nilai iterasinya masing-masing.Perbaikan ini menghapus satu kategori bug, tetapi jangan tertukar: loop variable aman tidak menyelamatkan state bersama — shared tetap berlomba tanpa terlibat penangkapan loop.
Perbaikan dengan Mutex #
Solusi paling umum: kunci akses state bersama. Berkas race-safe-demo.go memindahkan counter ke struct dengan sync.Mutex:
package main
import (
"fmt"
"sync"
)
// CounterAman melindungi field nilai dengan sync.Mutex,
// sehingga hanya satu goroutine yang boleh mengubahnya pada satu waktu.
type CounterAman struct {
mu sync.Mutex
nilai int
}
func (c *CounterAman) Tambah() {
c.mu.Lock()
defer c.mu.Unlock()
c.nilai++
}
func (c *CounterAman) Nilai() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.nilai
}
func main() {
var wg sync.WaitGroup
counter := &CounterAman{}
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for range 1250 {
counter.Tambah()
}
}()
}
wg.Wait()
fmt.Println("nilai counter:", counter.Nilai())
}Jalankan program tersebut dari direktori chapter:
go run race-safe-demo.goOutput yang Dihasilkan:
nilai counter: 10000Mari kita telaah baris demi baris program di atas:
- Baris 10–13 (
CounterAman): struct counter berfieldnilaidanmu— mutex menjadi bagian data yang dilindunginya. - Baris 15–19 (
Tambah):Lockmerebut kunci,defer Unlockmemastikan kunci terlepas, dannilai++kini berjalan di wilayah yang hanya boleh dimasuki satu goroutine. - Baris 21–25 (
Nilai): pembacaan juga dikunci agar tidak berlomba dengan penulisan. - Baris 31–39: delapan goroutine sama persis dengan versi berlomba — hanya
counter.Tambah()yang kini aman. - Baris 41–42: setelah
wg.Wait(), counter terbaca10000— benar di setiap eksekusi.
Kami menjalankan ulang versi ini dengan flag -race: hasilnya tetap nilai counter: 10000 tanpa laporan WARNING dan kode keluar 0 — konflik memori lenyap. Untuk counter murni, atomic dari chapter Atomic Operation
lebih murah:
var counter atomic.Int64
counter.Add(1) // penambahan aman tanpa mutexMutex tetap tepat saat beberapa field harus berubah konsisten bersamaan; atomic hanya mengikat satu operasi.
Kerusakan terakhir concurrency tanpa disiplin bukan di data, melainkan goroutine yang tertinggal hidup. Chapter penutup section ini memburu goroutine leak sampai ke akarnya.
Referensi #
- Go Blog: Introducing the Go Race Detector
— pengantar resmi flag
-race. - Data Race Detector — dokumentasi cara kerja dan biaya detector.
- The Go Memory Model — definisi resmi happens-before dan data race.
- For statements — Spesifikasi Go — semantik loop variable per iterasi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
race-demo.go
Unduh berkaspackage main
import (
"sync"
)
func main() {
var wg sync.WaitGroup
shared := 0
// Delapan goroutine menambah variabel yang sama tanpa proteksi.
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for range 1250 {
shared++ // data race: baca dan tulis bersamaan
}
}()
}
wg.Wait()
}
race-safe-demo.go
Unduh berkaspackage main
import (
"fmt"
"sync"
)
// CounterAman melindungi field nilai dengan sync.Mutex,
// sehingga hanya satu goroutine yang boleh mengubahnya pada satu waktu.
type CounterAman struct {
mu sync.Mutex
nilai int
}
func (c *CounterAman) Tambah() {
c.mu.Lock()
defer c.mu.Unlock()
c.nilai++
}
func (c *CounterAman) Nilai() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.nilai
}
func main() {
var wg sync.WaitGroup
counter := &CounterAman{}
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for range 1250 {
counter.Tambah()
}
}()
}
wg.Wait()
fmt.Println("nilai counter:", counter.Nilai())
}