← Semua pembelajaran / Go Nol → Enterprise
Fase 2 · Concurrency

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.

Sumber asli pkg.go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • Selalu defer mu.Unlock() tepat setelah mu.Lock(). Jalur error yang lupa membuka lock = server menggantung.
  • RWMutex mengizinkan 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 vet menangkap pelanggarannya.
  • sync.Once untuk inisialisasi sekali yang aman dari banyak goroutine.
  • sync/atomic untuk penghitung sederhana; atomic.Int64 lebih aman daripada fungsi lama atomic.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
}
AturanKalau dilanggar
defer Unlock() tepat setelah Lock()Jalur return di tengah meninggalkan lock terkunci — seluruh server menggantung
Method dengan mutex memakai receiver pointerSetiap panggilan mengunci salinan — lock tidak melindungi apa pun
Jangan kunci ulang mutex yang samaDeadlock: sync.Mutex tidak reentrant
Jangan panggil kode luar sambil memegang lockCallback 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

KebutuhanAlat
Map atau slice bersamasync.Mutex (atau RWMutex kalau baca jauh lebih sering)
Penghitung, flagatomic.Int64, atomic.Bool
Konfigurasi yang diganti utuh sesekaliatomic.Pointer[T] — pembaca tanpa lock sama sekali
Inisialisasi sekalisync.OnceValue
Menunggu sekelompok goroutinesync.WaitGroup
Membatasi jumlah yang berjalan bersamaangolang.org/x/sync/semaphore, atau channel berbuffer
Objek sementara yang sering dialokasisync.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.