Mutex (mutual exclusion) adalah kunci yang memastikan cuma satu goroutine yang menyentuh data bersama pada satu waktu. Bayangin buku catatan penjualan di meja kasir Warung Bakso Mas Bimo, dengan dua pelayan yang mencatat. Kalau dua-duanya membaca "41 porsi" di saat yang sama, lalu masing-masing menulis "42", satu penjualan hilang dari catatan. Solusinya: cuma ada satu pulpen. Siapa yang memegang pulpen boleh membaca dan menulis, yang lain menunggu sampai pulpennya dikembalikan. Pulpen itu mutex.
🧪 Tebakan cepat: 100 goroutine masing-masing memanggil
catat()10 kali, dancatat()cuma berisiterjual++. Apakah hasilnya selaluterjual: 1000? Tebak dulu, jawabannya ada di bagian "Race condition: penjualan yang hilang".
Race condition: penjualan yang hilang
package main
import (
"fmt"
"sync"
)
var terjual int
func catat() {
terjual++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
catat()
}
}()
}
wg.Wait()
fmt.Println("terjual:", terjual)
}
Program ini dijalankan 18 kali di Go Playground (8 CPU). 14 kali hasilnya terjual: 1000, tapi empat kali hasilnya terjual: 950, terjual: 990, terjual: 990, dan terjual: 970. Jadi gak selalu 1000. Itu jawaban tebakannya.
Kenapa? terjual++ kelihatan satu langkah, padahal tiga: baca nilainya, tambah satu, tulis lagi. Dua goroutine bisa saling salip di tengahnya:
| Langkah | Goroutine A | Goroutine B | terjual |
|---|---|---|---|
| 1 | baca: 41 | 41 | |
| 2 | baca: 41 | 41 | |
| 3 | tulis 42 | 42 | |
| 4 | tulis 42 | 42 |
Dua penjualan, tapi hitungannya cuma naik satu. Ini disebut race condition: dua goroutine atau lebih menyentuh variabel yang sama, minimal satu menulis, tanpa sinkronisasi. Bug-nya cuma muncul kalau jadwalnya pas saling salip, jadi kadang muncul, kadang gak. Itu yang bikin race susah dicari.
sync.Mutex: satu goroutine satu waktu
var (
terjual int
mu sync.Mutex
)
func catat() {
mu.Lock()
defer mu.Unlock()
terjual++
}
mu.Lock()mengambil kunci. Kalau kunci sedang dipegang goroutine lain, baris ini menunggu sampai kuncinya dilepas.defer mu.Unlock()melepas kunci saatcatatselesai, termasuk kalau terjadi panic ataureturndi tengah. PolaLock()laludefer Unlock()adalah cara standar di Go.- Bagian di antara Lock dan Unlock disebut critical section: di situ cuma satu goroutine boleh berada.
- Zero value
sync.Mutexsudah siap dipakai, gak perlu dibuat dengan fungsi apa pun.
Sekarang B yang datang saat A memegang kunci harus menunggu, lalu membaca 42, bukan 41. Hasilnya selalu terjual: 1000.
Di latihan: counter yang aman
Latihan lesson ini memakai pola yang sama dengan nama lain: variabel counter, mutex mu, dan fungsi increment(). main sudah menjalankan 100 goroutine yang masing-masing memanggil increment() 10 kali, lalu mencetak Counter: 1000. Tugasmu mengisi increment() dengan mu.Lock(), defer mu.Unlock(), dan counter++.
sync.RWMutex: banyak pembaca, satu penulis
Kalau datanya sering dibaca tapi jarang diubah, misalnya daftar harga, sync.RWMutex lebih cocok:
var (
harga = map[string]int{"bakso urat": 15000, "mie ayam": 12000, "es teh": 5000}
hmu sync.RWMutex
)
func lihatHarga(menu string) int {
hmu.RLock()
defer hmu.RUnlock()
return harga[menu]
}
func ubahHarga(menu string, baru int) {
hmu.Lock()
defer hmu.Unlock()
harga[menu] = baru
}
RLock boleh dipegang banyak pembaca sekaligus. Lock tetap eksklusif: penulis menunggu semua pembaca selesai, dan pembaca baru menunggu penulisnya. Kalau tulis sama seringnya dengan baca, sync.Mutex biasa lebih sederhana dan sering lebih cepat.
💡 Mendeteksi race dengan -race
Go punya race detector: go run -race bakso.go. Kalau ada race, program mencetak laporan yang diawali WARNING: DATA RACE, menyebut baca dan tulis yang bertabrakan beserta goroutine dan baris kodenya, lalu keluar dengan exit status 66. Jalankan test dengan go test -race juga. Map lebih galak lagi: menulis map dari banyak goroutine tanpa kunci bisa langsung mematikan program dengan fatal error: concurrent map writes.
⚠️ Jebakan yang sering ketemu
- Lock dua kali di goroutine yang sama. Mutex Go gak reentrant: Lock kedua menunggu dirinya sendiri, dan program mati dengan
fatal error: all goroutines are asleep - deadlock!. - Unlock tanpa Lock. Program langsung mati dengan
fatal error: sync: unlock of unlocked mutex. - Cek dan ubah di kunci terpisah. "Stok masih ada?" lalu "kurangi satu" harus di dalam satu critical section, kalau gak, dua goroutine bisa sama-sama menjual porsi terakhir.
- Menyalin mutex. Struct yang berisi mutex harus dikirim sebagai pointer. Salinannya punya kunci sendiri yang gak melindungi apa pun.
TL;DR: Race condition terjadi kalau dua goroutine atau lebih menyentuh data yang sama, minimal satu menulis, tanpa sinkronisasi; terjual++ itu baca, tambah, tulis, jadi update bisa hilang (di Go Playground: 950 dari 1000). sync.Mutex membuat cuma satu goroutine di critical section: mu.Lock(), defer mu.Unlock(), lalu ubah datanya. Pakai sync.RWMutex (RLock untuk baca, Lock untuk tulis) kalau datanya banyak dibaca dan jarang ditulis. Cari race dengan go run -race. Jangan Lock dua kali, jangan Unlock tanpa Lock, dan jangan menyalin mutex.