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:
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:
go run atomic-demo.goOutput yang Dihasilkan:
total kunjungan: 10000
saldo awal: 100
CAS pertama (100 → 150): true
CAS kedua (100 → 200): false
saldo akhir: 150
gaya lama AddInt64: 10Mari kita telaah baris demi baris program di atas:
- Baris 11 (
var kunjungan atomic.Int64): zero valueatomic.Int64sudah bernilai0dan langsung siap dipakai — tidak perlu inisialisasi khusus. - Baris 14–22 (goroutine): delapan goroutine masing-masing memanggil
kunjungan.Add(1)sebanyak 1250 kali; total maksimum 10000 penambahan. - Baris 19 (
kunjungan.Add(1)): penambahan atomik; berapa pun goroutine yang menambah pada saat bersamaan, tidak ada penambahan yang hilang. - Baris 23–24 (
wg.Wait()dan cetak): setelah semua goroutine selesai,Load()membaca nilai final — output10000ini identik di setiap eksekusi. - Baris 27–29 (
StoredanLoad): menulis dan membaca nilai secara atomik; penghapusan atau penggantian nilai selalu utuh. - Baris 32–36 (
CompareAndSwap): dua percobaan pertukaran bersyarat yang hasilnya dijelaskan pada bagian berikutnya. - Baris 39–41 (
atomic.AddInt64): gaya lama dengan pointer keint64biasa, 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:
| Kebutuhan | Gaya modern (Go 1.19+) | Gaya lama |
|---|---|---|
| Tambah counter | counter.Add(1) | atomic.AddInt64(&counter, 1) |
| Baca nilai | counter.Load() | atomic.LoadInt64(&counter) |
| Tulis nilai | counter.Store(10) | atomic.StoreInt64(&counter, 10) |
| Tukar bersyarat | counter.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:
| Skenario | Alat yang tepat |
|---|---|
| Counter atau flag sederhana | sync/atomic |
| Beberapa field yang harus konsisten bersamaan | sync.Mutex |
| Mengoordinasikan alur kerja antar goroutine | channel |
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 #
- Package sync/atomic — daftar lengkap tipe dan fungsi atomik.
- The Go Memory Model — garansi urutan untuk operasi atomik.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
atomic-demo.go
Unduh berkaspackage 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))
}