Slogan Go “share memory by communicating” menyarankan data berpindah lewat channel dari satu pemilik ke pemilik berikutnya. Namun sebagian data memang hidup sebagai shared state — counter permintaan, saldo kas, isi cache — yang dibaca dan ditulis banyak goroutine. Operasi paralel pada satu memori tanpa pengawalan menghasilkan data race: nilai hilang, hasil tak dapat direproduksi, dan go run -race berteriak. Chapter ini menghadirkan sync.Mutex sebagai penjaga akses eksklusif, lalu sync.RWMutex ketika pembaca jauh lebih banyak daripada penulis.

Data Race pada Counter #

Statement counter++ tampak satu langkah, tetapi mesin menjalankannya sebagai tiga: baca nilai counter, tambah satu, tulis kembali. Dua goroutine yang berjalan pada saat bersamaan bisa membaca nilai sama, sama-sama menambah, lalu menulis hasil sama — satu penambahan hilang tanpa jejak. Program penghitung tanpa pengawalan (5 goroutine × 1000 penambahan, total seharusnya 5000) kami jalankan sepuluh kali: hasilnya berganti-ganti — 5000, 4371, 4247 — berbeda tiap eksekusi. Dijalankan dengan go run -race, race detector melaporkan WARNING: DATA RACE beserta kedua goroutine yang saling menabrak. Bedah tuntas race detector menunggu chapter Race Condition ; tugas kita kini mencegahnya.

Mutex: Satu Pemilik pada Satu Waktu #

sync.Mutex (mutual exclusion) menjamin hanya satu goroutine berada di dalam wilayah terkunci: Lock() memblokir pemanggil berikutnya sampai pemegang kunci memanggil Unlock(). Berkas mutex-demo.go memakai mutex pada program penghitung tadi:

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	counter := 0

	var wg sync.WaitGroup
	wg.Add(5)

	for range 5 {
		go func() {
			defer wg.Done()
			for range 1000 {
				mu.Lock()
				counter++
				mu.Unlock()
			}
		}()
	}

	wg.Wait()
	fmt.Println("counter akhir:", counter)
}

Output yang Dihasilkan:

text
counter akhir: 5000

Mari kita telaah baris demi baris program di atas:

  1. Baris 9 (var mu sync.Mutex): mutex bernilai nol sudah terbuka dan siap dipakai tanpa konstruktor, seperti WaitGroup.
  2. Baris 12–13 (wg.Add(5)): counter WaitGroup dinaikkan sebelum kelima goroutine dimulai, mengikuti pola chapter sync.WaitGroup .
  3. Baris 15–24 (for range 5): lima goroutine mengerjakan seribu penambahan masing-masing; tanpa mutex semuanya bertabrakan di counter++.
  4. Baris 19–21 (mu.Lock() ... mu.Unlock()): wilayah di antara keduanya adalah critical section; operasi baca-tambah-tulis menjadi utuh di dalamnya sehingga tidak ada penambahan yang hilang.
  5. Baris 26–27 (wg.Wait() + cetak): hasil dibaca setelah seluruh goroutine selesai; selalu 5000 di setiap eksekusi, dan go run -race mutex-demo.go bersih tanpa laporan.

Pola yang lebih awet adalah defer mu.Unlock() tepat setelah Lock() — kunci pasti terlepas apa pun jalannya fungsi, termasuk saat return lebih awal. Kuncian juga harus dijaga sesingkat mungkin: kerja berat seperti I/O tidak boleh berada di dalam critical section karena semua goroutine lain menunggu di depan pintu.

RWMutex: Pembaca Banyak, Penulis Satu #

Ketika akses membaca jauh mendominasi menulis, sync.RWMutex menawarkan dua pintu: RLock() untuk pembaca dan Lock() untuk penulis. Banyak pembaca boleh masuk bersamaan; penulis masuk sendirian dan menutup pintu bagi semua. Berkas rwmutex-demo.go menerapkannya pada cache sederhana:

go
package main

import (
	"fmt"
	"sync"
)

// Cache memakai RWMutex: banyak pembaca boleh masuk bersamaan.
type Cache struct {
	mu   sync.RWMutex
	data map[string]int
}

// Set menulis nilai; hanya satu penulis pada satu waktu.
func (c *Cache) Set(kunci string, nilai int) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.data[kunci] = nilai
}

// Get membaca nilai; pembaca lain boleh membaca bersamaan.
func (c *Cache) Get(kunci string) (int, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	v, ok := c.data[kunci]
	return v, ok
}

func main() {
	cache := &Cache{data: make(map[string]int)}

	cache.Set("apel", 12000)
	cache.Set("jeruk", 8000)

	pembaca := []string{"Budi", "Siti", "Anto"}
	hasil := make([]string, len(pembaca))

	var wg sync.WaitGroup
	wg.Add(len(pembaca))
	for i, nama := range pembaca {
		go func() {
			defer wg.Done()
			harga, ok := cache.Get("apel")
			hasil[i] = fmt.Sprintf("%s membaca apel: %d (ada=%v)", nama, harga, ok)
		}()
	}

	wg.Wait()
	for _, h := range hasil {
		fmt.Println(h)
	}
}

Output yang Dihasilkan:

text
Budi membaca apel: 12000 (ada=true)
Siti membaca apel: 12000 (ada=true)
Anto membaca apel: 12000 (ada=true)

Mari kita telaah baris demi baris program di atas:

  1. Baris 9–12 (type Cache): struct menyimpan RWMutex dan map sebagai satu kesatuan state terlindungi — idiom “mutex menempel pada data yang dijaganya”.
  2. Baris 15–19 (Set): penulis memakai Lock()/Unlock() eksklusif; defer memastikan kunci lepas meski penulisan gagal di tengah jalan.
  3. Baris 22–27 (Get): pembaca memakai RLock(); ketiga goroutine pembaca boleh memasuki Get bersamaan — keuntungan dibanding mutex biasa pada beban baca dominan.
  4. Baris 40–46 (go func): tiap pembaca menulis ke slot indeks sendiri di slice hasil — bebas race; pencetakan oleh main setelah Wait menjadikan output terurut stabil.
  5. Baris 43 (cache.Get("apel")): hasil tiap pembaca sama-sama 12000 dan ada=true — pembacaan paralel tidak saling merusak.

RWMutex bukan gratis: dua pintu menambah biaya kecil, jadi pakai hanya bila bagian baca benar-benar dominan dan critical section-nya tidak sepele. Sejak Go 1.18 tersedia pula TryLock() dan TryRLock() yang mencoba mengunci tanpa memblokir — mengembalikan false bila kunci sedang dipegang pihak lain; berguna untuk instrumentasi.

Mutex atau Channel, Kapan yang Mana #

Effective Go meringkasnya: “Share memory by communicating” — data yang bisa diantar, antarlah lewat channel. Pedoman praktis: channel menang untuk orkestrasi dan aliran data antar goroutine; mutex menang untuk menjaga state dengan operasi kecil (counter, map, konfigurasi). Keduanya sah dipadukan: pipeline channel di luar, mutex menjaga struktur data bersama di dalam. Untuk map dengan kunci yang jarang tumpang tindih dan akses mayoritas baca, standard library menyediakan sync.Map; untuk kebutuhan umum, map biasa plus RWMutex lebih mudah dianalisis. Apa pun alatnya, kebenarannya berakar pada Go memory model: akses yang disinkronkan menciptakan relasi happens-before sehingga pembaruan satu goroutine pasti terlihat oleh lainnya.

Referensi #

Kotak alat concurrency dasar — goroutine, channel, select, WaitGroup, mutex — kini lengkap. Bab berikutnya menutup satu kebutuhan yang sering menyertai state terlindungi: menjalankan inisialisasi tepat satu kali lewat sync.Once.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

mutex-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	counter := 0

	var wg sync.WaitGroup
	wg.Add(5)

	for range 5 {
		go func() {
			defer wg.Done()
			for range 1000 {
				mu.Lock()
				counter++
				mu.Unlock()
			}
		}()
	}

	wg.Wait()
	fmt.Println("counter akhir:", counter)
}

rwmutex-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"sync"
)

// Cache memakai RWMutex: banyak pembaca boleh masuk bersamaan.
type Cache struct {
	mu   sync.RWMutex
	data map[string]int
}

// Set menulis nilai; hanya satu penulis pada satu waktu.
func (c *Cache) Set(kunci string, nilai int) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.data[kunci] = nilai
}

// Get membaca nilai; pembaca lain boleh membaca bersamaan.
func (c *Cache) Get(kunci string) (int, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	v, ok := c.data[kunci]
	return v, ok
}

func main() {
	cache := &Cache{data: make(map[string]int)}

	cache.Set("apel", 12000)
	cache.Set("jeruk", 8000)

	pembaca := []string{"Budi", "Siti", "Anto"}
	hasil := make([]string, len(pembaca))

	var wg sync.WaitGroup
	wg.Add(len(pembaca))
	for i, nama := range pembaca {
		go func() {
			defer wg.Done()
			harga, ok := cache.Get("apel")
			hasil[i] = fmt.Sprintf("%s membaca apel: %d (ada=%v)", nama, harga, ok)
		}()
	}

	wg.Wait()
	for _, h := range hasil {
		fmt.Println(h)
	}
}