Counter bersama adalah masalah concurrency terkecil yang paling sering dijumpai: jumlah request yang dilayani, jumlah klik, jumlah item yang diproses. Mutex dari chapter sebelumnya bisa menyelesaikannya, tetapi mengunci dan membuka kunci hanya demi satu angka terasa berlebihan. Package sync/atomic menyediakan operasi yang jauh lebih murah: tambah, baca, tulis, dan tukar yang dijamin utuh sebagai satu kesatuan, biasanya cukup satu instruksi prosesor. Chapter ini membahas tipe modern seperti atomic.Int64, operasi CompareAndSwap, gaya lama berbasis pointer, dan kapan atomic lebih tepat daripada mutex.

Counter Tanpa Mutex #

Persoalannya ada pada pernyataan shared++. Operator tersebut sebenarnya tiga langkah: baca nilai dari memori, tambah satu di register, lalu tulis kembali ke memori. Dua goroutine yang menjalankan urutan ini pada saat bersamaan bisa membaca nilai yang sama, dan salah satu penambahan hilang. Tipe atomic.Int64 menjamin penambahan tidak pernah terpotong. Berikut berkas atomic-demo.go:

go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	// Gaya modern: tipe atomic.Int64 lebih aman daripada fungsi pointer.
	var kunjungan atomic.Int64
	var wg sync.WaitGroup

	for range 8 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range 1250 {
				kunjungan.Add(1)
			}
		}()
	}
	wg.Wait()
	fmt.Println("total kunjungan:", kunjungan.Load())

	// Store dan Load: tulis dan baca nilai secara atomik.
	saldo := atomic.Int64{}
	saldo.Store(100)
	fmt.Println("saldo awal:", saldo.Load())

	// CompareAndSwap: ganti nilai hanya bila nilai lama masih sesuai.
	berhasil := saldo.CompareAndSwap(100, 150)
	fmt.Println("CAS pertama (100 → 150):", berhasil)
	berhasil = saldo.CompareAndSwap(100, 200)
	fmt.Println("CAS kedua (100 → 200):", berhasil)
	fmt.Println("saldo akhir:", saldo.Load())

	// Gaya lama: fungsi package atomic yang menerima pointer.
	var gayaLama int64
	atomic.AddInt64(&gayaLama, 10)
	fmt.Println("gaya lama AddInt64:", atomic.LoadInt64(&gayaLama))
}

Jalankan program tersebut dari direktori chapter:

bash
go run atomic-demo.go

Output yang Dihasilkan:

text
total kunjungan: 10000
saldo awal: 100
CAS pertama (100 → 150): true
CAS kedua (100 → 200): false
saldo akhir: 150
gaya lama AddInt64: 10

Mari kita telaah baris demi baris program di atas:

  1. Baris 11 (var kunjungan atomic.Int64): zero value atomic.Int64 sudah bernilai 0 dan langsung siap dipakai — tidak perlu inisialisasi khusus.
  2. Baris 14–22 (goroutine): delapan goroutine masing-masing memanggil kunjungan.Add(1) sebanyak 1250 kali; total maksimum 10000 penambahan.
  3. Baris 19 (kunjungan.Add(1)): penambahan atomik; berapa pun goroutine yang menambah pada saat bersamaan, tidak ada penambahan yang hilang.
  4. Baris 23–24 (wg.Wait() dan cetak): setelah semua goroutine selesai, Load() membaca nilai final — output 10000 ini identik di setiap eksekusi.
  5. Baris 27–29 (Store dan Load): menulis dan membaca nilai secara atomik; penghapusan atau penggantian nilai selalu utuh.
  6. Baris 32–36 (CompareAndSwap): dua percobaan pertukaran bersyarat yang hasilnya dijelaskan pada bagian berikutnya.
  7. Baris 39–41 (atomic.AddInt64): gaya lama dengan pointer ke int64 biasa, ditampilkan sebagai pembanding.

CompareAndSwap: Pembaruan Optimis #

CompareAndSwap(old, new) mengganti nilai hanya bila nilai yang tersimpan masih sama dengan old. Operasi ini mengembalikan bool: true bila penggantian terjadi, false bila nilai sudah berubah di tangan pihak lain. Pada contoh di atas, saldo mula-mula disimpan 100. CAS pertama meminta “ganti 100 menjadi 150” dan berhasil. CAS kedua masih meminta “ganti 100 menjadi 200”, tetapi saldo sudah menjadi 150, sehingga CAS kedua gagal dan saldo tetap 150.

Pola ini disebut pembaruan optimis: lakukan pekerjaan, coba komit, dan ulangi bila pihak lain lebih dulu. Membungkus CAS dalam perulangan menjadi fondasi berbagai algoritma tanpa kunci, dari antrian lock-free sampai struktur data concurrent.

Gaya Lama: Fungsi Package atomic #

Sebelum Go 1.19, atomic hanya tersedia sebagai fungsi yang menerima pointer: atomic.AddInt64(&v, 1), atomic.LoadInt64(&v), dan atomic.CompareAndSwapInt64(&v, lama, baru). Tabel berikut memetakan kebutuhan yang sama pada kedua gaya:

KebutuhanGaya modern (Go 1.19+)Gaya lama
Tambah countercounter.Add(1)atomic.AddInt64(&counter, 1)
Baca nilaicounter.Load()atomic.LoadInt64(&counter)
Tulis nilaicounter.Store(10)atomic.StoreInt64(&counter, 10)
Tukar bersyaratcounter.CompareAndSwap(lama, baru)atomic.CompareAndSwapInt64(&counter, lama, baru)

Gaya modern lebih disarankan karena keseluruhan nilai tinggal di dalam satu tipe: tidak ada pointer yang bisa tidak sengaja menunjuk variabel yang disalin, dan tidak ada cara membaca nilai tanpa jalur atomik. Keluarga tipe ini juga lengkap: atomic.Bool, atomic.Int32, atomic.Uint32, atomic.Uint64, hingga atomic.Pointer[T] untuk pointer generik dan atomic.Value untuk nilai bertipe bebas.

Atomic atau Mutex? #

Atomic cocok untuk keadaan yang benar-benar tunggal: satu counter, satu flag, satu nilai yang sering dibaca dan jarang ditulis. Mutex tetap unggul begitu ada lebih dari satu variabel yang harus berubah bersamaan, karena atomic tidak mengikat beberapa operasi menjadi satu transaksi. Perbandingan praktisnya:

SkenarioAlat yang tepat
Counter atau flag sederhanasync/atomic
Beberapa field yang harus konsisten bersamaansync.Mutex
Mengoordinasikan alur kerja antar goroutinechannel

Untuk counter murni, atomic adalah pilihan termurah dan paling sulit salah. Begitu kebutuhan bergerak ke arah pembatalan operasi lintas banyak goroutine, kita membutuhkan alat dengan jangkauan lebih luas — chapter berikutnya memperkenalkan package context.

Referensi #

Berkas Sumber

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

atomic-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	// Gaya modern: tipe atomic.Int64 lebih aman daripada fungsi pointer.
	var kunjungan atomic.Int64
	var wg sync.WaitGroup

	for range 8 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range 1250 {
				kunjungan.Add(1)
			}
		}()
	}
	wg.Wait()
	fmt.Println("total kunjungan:", kunjungan.Load())

	// Store dan Load: tulis dan baca nilai secara atomik.
	saldo := atomic.Int64{}
	saldo.Store(100)
	fmt.Println("saldo awal:", saldo.Load())

	// CompareAndSwap: ganti nilai hanya bila nilai lama masih sesuai.
	berhasil := saldo.CompareAndSwap(100, 150)
	fmt.Println("CAS pertama (100 → 150):", berhasil)
	berhasil = saldo.CompareAndSwap(100, 200)
	fmt.Println("CAS kedua (100 → 200):", berhasil)
	fmt.Println("saldo akhir:", saldo.Load())

	// Gaya lama: fungsi package atomic yang menerima pointer.
	var gayaLama int64
	atomic.AddInt64(&gayaLama, 10)
	fmt.Println("gaya lama AddInt64:", atomic.LoadInt64(&gayaLama))
}