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:
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:
counter akhir: 5000Mari kita telaah baris demi baris program di atas:
- Baris 9 (
var mu sync.Mutex): mutex bernilai nol sudah terbuka dan siap dipakai tanpa konstruktor, seperti WaitGroup. - Baris 12–13 (
wg.Add(5)): counter WaitGroup dinaikkan sebelum kelima goroutine dimulai, mengikuti pola chapter sync.WaitGroup . - Baris 15–24 (
for range 5): lima goroutine mengerjakan seribu penambahan masing-masing; tanpa mutex semuanya bertabrakan dicounter++. - 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. - Baris 26–27 (
wg.Wait()+ cetak): hasil dibaca setelah seluruh goroutine selesai; selalu5000di setiap eksekusi, dango run -race mutex-demo.gobersih 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:
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:
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:
- Baris 9–12 (
type Cache): struct menyimpan RWMutex dan map sebagai satu kesatuan state terlindungi — idiom “mutex menempel pada data yang dijaganya”. - Baris 15–19 (
Set): penulis memakaiLock()/Unlock()eksklusif;defermemastikan kunci lepas meski penulisan gagal di tengah jalan. - Baris 22–27 (
Get): pembaca memakaiRLock(); ketiga goroutine pembaca boleh memasukiGetbersamaan — keuntungan dibanding mutex biasa pada beban baca dominan. - Baris 40–46 (
go func): tiap pembaca menulis ke slot indeks sendiri di slicehasil— bebas race; pencetakan olehmainsetelahWaitmenjadikan output terurut stabil. - Baris 43 (
cache.Get("apel")): hasil tiap pembaca sama-sama12000danada=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 #
- sync.Mutex — dokumentasi resmi mutex dan TryLock.
- sync.RWMutex — dokumentasi resmi read-write mutex.
- Concurrency — Effective Go — filosofi share memory by communicating.
- Introducing the Go Race Detector — The Go Blog — mengenal cara kerja deteksi data race.
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 berkaspackage 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 berkaspackage 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)
}
}