Mutex, atomic & sync
Untuk sebagian besar keadaan bersama di aplikasi web — cache, penghitung, peta koneksi — mutex adalah jawaban yang lebih sederhana dan lebih cepat daripada channel.
Intisari
- Selalu
defer mu.Unlock()tepat setelahmu.Lock(). Jalur error yang lupa membuka lock = server menggantung. RWMutexmengizinkan banyak pembaca bersamaan; berguna hanya kalau bacaan jauh lebih sering daripada tulisan.- Mutex tidak boleh disalin. Selalu simpan di struct yang dioper sebagai pointer —
go vetmenangkap pelanggarannya. sync.Onceuntuk inisialisasi sekali yang aman dari banyak goroutine.sync/atomicuntuk penghitung sederhana;atomic.Int64lebih aman daripada fungsi lamaatomic.AddInt64.
Bentuk yang benar
type Cache struct {
mu sync.RWMutex // konvensi: tepat di atas yang dilindunginya
data map[string]Produk
}
func NewCache() *Cache { // pointer — struct berisi mutex TIDAK BOLEH disalin
return &Cache{data: make(map[string]Produk)}
}
func (c *Cache) Ambil(k string) (Produk, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
p, ok := c.data[k]
return p, ok
}
func (c *Cache) Simpan(k string, p Produk) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[k] = p
}
| Aturan | Kalau dilanggar |
|---|---|
defer Unlock() tepat setelah Lock() | Jalur return di tengah meninggalkan lock terkunci — seluruh server menggantung |
| Method dengan mutex memakai receiver pointer | Setiap panggilan mengunci salinan — lock tidak melindungi apa pun |
| Jangan kunci ulang mutex yang sama | Deadlock: sync.Mutex tidak reentrant |
| Jangan panggil kode luar sambil memegang lock | Callback yang mengunci lagi = deadlock; panggilan lambat = semua tertahan |
sync.Mutex tidak reentrant — sengaja. Method yang memegang lock lalu memanggil
method lain yang juga mengunci akan mati di tempat. Perbaikannya: pecah jadi fungsi privat tanpa
penguncian (ambilTanpaLock) yang dipanggil dari fungsi bermutex.
Mutex versus RWMutex
mu.Lock() / mu.Unlock() // eksklusif — satu goroutine
mu.RLock() / mu.RUnlock() // banyak pembaca boleh bersamaan
RWMutex terlihat selalu lebih baik, tapi tidak. Ia punya overhead lebih besar per operasi,
jadi ia hanya menang kalau bagian yang dilindungi cukup lama dan bacaannya jauh lebih sering daripada
tulisan. Untuk penghitung atau map kecil yang diakses sebentar, Mutex biasa biasanya lebih
cepat. Ukur dengan go test -bench (Fase 8) sebelum memilih.
sync.Once: inisialisasi sekali
var (
sekali sync.Once
klien *http.Client
)
func Klien() *http.Client {
sekali.Do(func() {
klien = &http.Client{Timeout: 5 * time.Second}
})
return klien
}
Berapa pun goroutine yang memanggil bersamaan, fungsi di dalam Do berjalan tepat sekali, dan
semua pemanggil menunggu sampai ia selesai. Ada juga sync.OnceValue dan
sync.OnceValues (Go 1.21+) yang lebih ringkas untuk pola ini:
var Klien = sync.OnceValue(func() *http.Client {
return &http.Client{Timeout: 5 * time.Second}
})
Untuk aplikasi, biasanya kamu tidak butuh ini sama sekali. Bangun dependensi di
main dan oper lewat konstruktor (Fase 5). sync.Once berguna untuk pustaka dan
untuk hal yang benar-benar mahal dan mungkin tidak pernah dipakai.
sync/atomic: penghitung tanpa lock
type Statistik struct {
permintaan atomic.Int64 // tipe atomik — Go 1.19+
gagal atomic.Int64
siap atomic.Bool
}
func (s *Statistik) Catat(err error) {
s.permintaan.Add(1)
if err != nil {
s.gagal.Add(1)
}
}
func (s *Statistik) Baca() (int64, int64) {
return s.permintaan.Load(), s.gagal.Load()
}
Pakai tipe atomic.Int64, bukan fungsi lama atomic.AddInt64(&n, 1).
Tipe atomik tidak bisa dibaca atau ditulis secara tidak sengaja tanpa method atomik, dan ia menjamin
perataan memori yang benar di arsitektur 32-bit — sumber crash yang membingungkan pada versi lama.
Batas atomic
// ❌ dua operasi atomik BUKAN satu operasi atomik
if kuota.Load() > 0 {
kuota.Add(-1) // goroutine lain bisa menyelinap di antara dua baris ini
}
// ✅ mutex, atau operasi bandingkan-dan-tukar
for {
lama := kuota.Load()
if lama == 0 {
return ErrKuotaHabis
}
if kuota.CompareAndSwap(lama, lama-1) {
break
}
}
Ringkasan pilihan
| Kebutuhan | Alat |
|---|---|
| Map atau slice bersama | sync.Mutex (atau RWMutex kalau baca jauh lebih sering) |
| Penghitung, flag | atomic.Int64, atomic.Bool |
| Konfigurasi yang diganti utuh sesekali | atomic.Pointer[T] — pembaca tanpa lock sama sekali |
| Inisialisasi sekali | sync.OnceValue |
| Menunggu sekelompok goroutine | sync.WaitGroup |
| Membatasi jumlah yang berjalan bersamaan | golang.org/x/sync/semaphore, atau channel berbuffer |
| Objek sementara yang sering dialokasi | sync.Pool — hanya setelah profiling membuktikannya |
sync.Map jarang jadi jawaban. Ia dioptimalkan untuk dua pola sempit: kunci yang
ditulis sekali lalu dibaca berkali-kali, dan goroutine yang mengakses kumpulan kunci yang tidak
bertumpang tindih. Di luar itu, map biasa dengan RWMutex lebih cepat
dan bertipe. Mulai dari yang biasa.
Latihan: tulis Cache seperti contoh di atas, lalu panggil Simpan dari
50 goroutine sekaligus tanpa mutex dan jalankan go run -race . — baca laporan race-nya
sampai habis. Pasang mutexnya, jalankan lagi, dan pastikan bersih. Terakhir, ubah receiver method jadi
nilai (func (c Cache)) dan lihat apa yang dikatakan go vet.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.