Sync Atomic #
Package sync/atomic menyediakan operasi tingkat rendah yang dijamin berjalan secara atomik — artinya operasi tersebut tidak bisa diinterupsi di tengah jalan oleh goroutine lain. Ini berbeda dari Mutex yang mengunci seluruh critical section: operasi atomik bekerja pada level instruksi CPU, sehingga jauh lebih cepat. Untuk kasus sederhana seperti counter, flag boolean, atau cache yang sering dibaca — di mana Mutex terasa berlebihan — atomic adalah pilihan yang tepat. Go 1.19 memperkenalkan tipe generik seperti atomic.Int64, atomic.Bool, dan atomic.Pointer[T] yang jauh lebih ergonomis dari fungsi-fungsi tingkat rendah sebelumnya. Memahami kapan menggunakan atomic vs Mutex adalah kunci untuk menulis kode concurrent yang benar sekaligus efisien.
Gambaran Besar Package sync/atomic #
flowchart TD
A["package sync/atomic"] --> OldAPI["API Lama (fungsi)\nGo 1.x"]
A --> NewAPI["API Baru (tipe)\nGo 1.19+"]
OldAPI --> OF1["atomic.AddInt32/64\natomic.LoadInt32/64\natomic.StoreInt32/64\natomic.SwapInt32/64\natomic.CompareAndSwapInt32/64"]
OldAPI --> OF2["atomic.LoadPointer\natomic.StorePointer\natomic.LoadUintptr"]
OldAPI --> OV["atomic.Value\nStore / Load / Swap\nCompareAndSwap"]
NewAPI --> NT1["atomic.Int32 / Int64\natomic.Uint32 / Uint64\natomic.Uintptr"]
NewAPI --> NT2["atomic.Bool\nStore / Load / Swap\nCompareAndSwap"]
NewAPI --> NT3["atomic.Pointer[T]\nStore / Load / Swap\nCompareAndSwap"]
subgraph Kapan["Gunakan atomic jika:"]
K1["Counter sederhana\n(hit count, request count)"]
K2["Flag boolean\n(shutdown, initialized)"]
K3["Pointer swap atomik\n(hot reload config)"]
K4["Operasi yang sangat sering\ndan Mutex terlalu mahal"]
end
style A fill:#4f86c6,color:#fff
style OldAPI fill:#fff3e0
style NewAPI fill:#e8f5e9
style Kapan fill:#e3f2fdAPI Baru — Tipe Atomik (Go 1.19+) #
Go 1.19 memperkenalkan tipe atomik yang lebih ergonomis. Gunakan ini untuk project baru:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// atomic.Int64 — counter yang aman untuk concurrent access
var counter atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Add(1)
}()
}
wg.Wait()
fmt.Println("Counter:", counter.Load()) // selalu 1000
// atomic.Bool — flag boolean
var shutdown atomic.Bool
shutdown.Store(false)
go func() {
// Simulasi shutdown signal
shutdown.Store(true)
}()
// Swap — set nilai baru, kembalikan nilai lama
sebelumnya := shutdown.Swap(true)
fmt.Println("Sebelumnya:", sebelumnya)
// CompareAndSwap — hanya ubah jika nilai saat ini sesuai
berhasil := shutdown.CompareAndSwap(true, false)
fmt.Println("CAS berhasil:", berhasil)
fmt.Println("Sekarang:", shutdown.Load())
// atomic.Pointer[T] — pointer yang aman
type Config struct {
Host string
Port int
}
var configPtr atomic.Pointer[Config]
configPtr.Store(&Config{"localhost", 8080})
cfg := configPtr.Load()
fmt.Printf("Config: %s:%d\n", cfg.Host, cfg.Port)
}
Semua Tipe Atomik Baru #
// Integer
var i32 atomic.Int32
var i64 atomic.Int64
var u32 atomic.Uint32
var u64 atomic.Uint64
var uptr atomic.Uintptr
// Boolean
var b atomic.Bool
// Pointer generik
var p atomic.Pointer[MyStruct]
// Method yang sama untuk semua:
// .Load() T — baca nilai saat ini
// .Store(val T) — simpan nilai baru
// .Swap(val T) T — simpan val, kembalikan nilai lama
// .CompareAndSwap(old, new T) bool — ubah hanya jika == old
// .Add(delta T) T — tambahkan delta, kembalikan nilai baru (hanya numerik)
API Lama — Fungsi Atomik #
Sebelum Go 1.19, operasi atomik dilakukan via fungsi. Masih valid dan sering ditemukan di kode yang ada:
import "sync/atomic"
// AddInt64 — tambah delta secara atomik, kembalikan nilai baru
var counter int64
atomic.AddInt64(&counter, 1) // counter++
atomic.AddInt64(&counter, -1) // counter--
atomic.AddInt64(&counter, 10) // counter += 10
// LoadInt64 — baca nilai secara atomik
val := atomic.LoadInt64(&counter)
// StoreInt64 — simpan nilai secara atomik
atomic.StoreInt64(&counter, 0) // reset
// SwapInt64 — simpan dan kembalikan nilai lama
sebelumnya := atomic.SwapInt64(&counter, 100)
// CompareAndSwapInt64 (CAS) — ubah hanya jika sama dengan old
berhasil := atomic.CompareAndSwapInt64(&counter, 100, 200)
// counter berubah ke 200 hanya jika saat ini == 100
// Versi 32-bit
var c32 int32
atomic.AddInt32(&c32, 1)
atomic.LoadInt32(&c32)
atomic.StoreInt32(&c32, 0)
atomic.CompareAndSwapInt32(&c32, 0, 1)
// Unsigned
var uc uint64
atomic.AddUint64(&uc, 1)
atomic.LoadUint64(&uc)
atomic.Value — Menyimpan Nilai Apapun #
atomic.Value memungkinkan penyimpanan dan pembacaan nilai bertipe interface{} secara atomik — berguna untuk hot-reload konfigurasi atau data yang sering dibaca:
sequenceDiagram
participant Writer as Goroutine Penulis
participant AV as atomic.Value
participant R1 as Reader 1
participant R2 as Reader 2
participant R3 as Reader 3
R1->>AV: Load() → config v1
R2->>AV: Load() → config v1
Writer->>AV: Store(config v2)
R3->>AV: Load() → config v2
R1->>AV: Load() → config v2
Note over AV: Tidak ada lock, tidak ada blocking\nSemua operasi atomicimport "sync/atomic"
// atomic.Value untuk hot-reload konfigurasi
type Config struct {
Host string
Port int
Debug bool
MaxConn int
}
var globalConfig atomic.Value
func init() {
// Simpan config awal
globalConfig.Store(&Config{
Host: "localhost",
Port: 8080,
Debug: false,
MaxConn: 100,
})
}
// Baca config — sangat cepat, tidak ada lock
func getConfig() *Config {
return globalConfig.Load().(*Config)
}
// Update config — biasanya dari goroutine background
func updateConfig(cfg *Config) {
globalConfig.Store(cfg)
}
// Hot-reload dari file
func watchConfig(path string, ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
cfg, err := muatConfigDariFile(path)
if err != nil {
log.Printf("gagal reload config: %v", err)
continue
}
updateConfig(cfg)
log.Println("config berhasil di-reload")
}
}
}
// Penggunaan di handler — sangat efisien
func handlerAPI(w http.ResponseWriter, r *http.Request) {
cfg := getConfig() // baca config tanpa lock
if cfg.Debug {
log.Printf("request: %s %s", r.Method, r.URL.Path)
}
// ...
}
Aturan atomic.Value #
var v atomic.Value
// ATURAN 1: tipe yang disimpan harus konsisten
v.Store(42) // simpan int
v.Store("string") // PANIC! tipe berubah dari int ke string
// BENAR: selalu simpan tipe yang sama
v.Store(42)
v.Store(100) // OK — keduanya int
// ATURAN 2: tidak boleh Store nil interface
var cfg *Config = nil
v.Store(cfg) // PANIC! tidak boleh Store nil
// BENAR: store pointer ke zero value
v.Store(&Config{}) // OK — pointer yang valid
// ATURAN 3: Load sebelum Store pertama mengembalikan nil
var v2 atomic.Value
result := v2.Load() // nil — belum pernah Store
if result != nil {
cfg := result.(*Config)
_ = cfg
}
// Swap — store dan kembalikan nilai lama (atomic)
lama := v.Swap(200)
fmt.Println(lama) // 100 (nilai sebelumnya)
// CompareAndSwap — ubah hanya jika sama dengan old
berhasil := v.CompareAndSwap(200, 300)
fmt.Println(berhasil) // true
Perbandingan: atomic vs Mutex #
flowchart TD
Q{"Apa yang ingin\ndisinkronisasi?"} --> Simple["Nilai tunggal\n(int, bool, pointer)"]
Q --> Complex["Beberapa nilai\nyang harus konsisten\nsatu sama lain"]
Q --> ReadHeavy["Baca sangat sering\ntulis jarang"]
Q --> WriteHeavy["Tulis sama seringnya\ndengan baca"]
Simple --> Atomic["sync/atomic\nLebih cepat\nLebih sederhana untuk nilai tunggal"]
Complex --> Mutex["sync.Mutex\nHarus satu atomic unit"]
ReadHeavy --> RWMutex["sync.RWMutex\natau atomic.Value"]
WriteHeavy --> Mutex2["sync.Mutex\natau atomic jika hanya satu nilai"]
style Atomic fill:#e8f5e9
style Mutex fill:#e3f2fd
style RWMutex fill:#e3f2fd
style Mutex2 fill:#e3f2fd// Perbandingan performa (ilustrasi benchmark):
// atomic.AddInt64: ~5 ns/op
// Mutex.Lock + nilai++ + Mutex.Unlock: ~25 ns/op
// (~5x lebih cepat untuk operasi tunggal)
// ANTI-PATTERN: atomic untuk operasi yang harus konsisten bersama
var total int64
var count int64
// Ini TIDAK aman! total dan count bisa tidak konsisten
// (goroutine lain bisa membaca di antara dua Add)
atomic.AddInt64(&total, harga)
atomic.AddInt64(&count, 1)
// rata-rata := total/count bisa tidak konsisten!
// BENAR: Mutex untuk beberapa nilai yang harus konsisten bersama
var mu sync.Mutex
var total2 int64
var count2 int64
mu.Lock()
total2 += harga
count2++
mu.Unlock()
// total2 dan count2 selalu konsisten
// BENAR: atomic untuk nilai tunggal yang independen
var requestCount atomic.Int64
var errorCount atomic.Int64
// Keduanya independen — boleh atomic
requestCount.Add(1)
if err != nil {
errorCount.Add(1)
}
Compare-And-Swap (CAS) — Operasi Kunci #
CAS adalah operasi fundamental dalam pemrograman concurrent lock-free. Ia mengubah nilai hanya jika nilai saat ini sama dengan yang diharapkan:
flowchart TD
CAS["CompareAndSwap(old, new)"] --> Check{"nilai saat ini\n== old?"}
Check -- Ya --> Update["Set nilai ke new\nKembalikan true"]
Check -- Tidak --> NoUpdate["Tidak ada perubahan\nKembalikan false"]
subgraph Contoh["Contoh: Increment Lock-Free"]
L1["Load nilai saat ini: n"]
L2["Hitung n+1"]
L3["CAS(n, n+1)"]
L4{berhasil?}
L5["Selesai"]
L6["Ulangi (loop)"]
L1 --> L2 --> L3 --> L4
L4 -- Ya --> L5
L4 -- Tidak --> L6 --> L1
end
style Update fill:#e8f5e9
style NoUpdate fill:#fce4ec// Pola CAS loop — untuk operasi yang membutuhkan baca-modifikasi-tulis atomik
func incrementSafe(counter *int64) {
for {
lama := atomic.LoadInt64(counter)
baru := lama + 1
if atomic.CompareAndSwapInt64(counter, lama, baru) {
return // berhasil
}
// gagal — nilai berubah di antara Load dan CAS — ulangi
}
}
// Dengan tipe baru — jauh lebih bersih
var counter atomic.Int64
counter.Add(1) // Add sudah CAS loop di dalamnya
// CAS untuk state machine — hanya izinkan transisi yang valid
type State int32
const (
StateIdle State = 0
StateRunning State = 1
StateStopped State = 2
)
var state atomic.Int32
func mulai() bool {
// Hanya bisa mulai jika saat ini Idle
return state.CompareAndSwap(int32(StateIdle), int32(StateRunning))
}
func berhenti() bool {
// Hanya bisa berhenti jika saat ini Running
return state.CompareAndSwap(int32(StateRunning), int32(StateStopped))
}
// Penggunaan
if mulai() {
fmt.Println("berhasil mulai")
} else {
fmt.Println("tidak bisa mulai — bukan dalam state Idle")
}
Pola Penggunaan di Produksi #
Counter Metrik yang Efisien #
// Counter metrik untuk monitoring — sering diupdate, sering dibaca
type Metrics struct {
RequestTotal atomic.Int64
RequestError atomic.Int64
BytesReceived atomic.Int64
BytesSent atomic.Int64
ActiveConns atomic.Int64
}
var metrics Metrics
func handlerWithMetrics(w http.ResponseWriter, r *http.Request) {
metrics.RequestTotal.Add(1)
metrics.ActiveConns.Add(1)
defer metrics.ActiveConns.Add(-1)
// Hitung bytes yang diterima
body, err := io.ReadAll(r.Body)
if err != nil {
metrics.RequestError.Add(1)
http.Error(w, "error", 500)
return
}
metrics.BytesReceived.Add(int64(len(body)))
// Proses dan kirim respons
respons := []byte(`{"status":"ok"}`)
w.Write(respons)
metrics.BytesSent.Add(int64(len(respons)))
}
// Endpoint untuk ekspos metrik
func handlerMetrics(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "request_total %d\n", metrics.RequestTotal.Load())
fmt.Fprintf(w, "request_error %d\n", metrics.RequestError.Load())
fmt.Fprintf(w, "bytes_received %d\n", metrics.BytesReceived.Load())
fmt.Fprintf(w, "bytes_sent %d\n", metrics.BytesSent.Load())
fmt.Fprintf(w, "active_connections %d\n", metrics.ActiveConns.Load())
}
Singleton dengan Once vs atomic.Pointer #
// Pendekatan 1: sync.Once — inisialisasi sekali
var (
dbOnce sync.Once
dbInstance *sql.DB
)
func GetDB() *sql.DB {
dbOnce.Do(func() {
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
dbInstance = db
})
return dbInstance
}
// Pendekatan 2: atomic.Pointer — bisa di-update (untuk hot-reload)
var dbPtr atomic.Pointer[sql.DB]
func GetDBv2() *sql.DB {
return dbPtr.Load()
}
func SetDB(db *sql.DB) {
dbPtr.Store(db)
}
// Berguna untuk test — bisa swap DB dengan test DB
func TestHandler(t *testing.T) {
testDB := setupTestDB(t)
SetDB(testDB)
defer SetDB(productionDB()) // restore setelah test
// ...
}
Rate Limiter Lock-Free #
// Rate limiter sederhana menggunakan atomic
type RateLimiter struct {
limit int64
window time.Duration
count atomic.Int64
resetAt atomic.Int64 // Unix nano
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
limit: int64(limit),
window: window,
}
rl.resetAt.Store(time.Now().Add(window).UnixNano())
return rl
}
func (rl *RateLimiter) Allow() bool {
now := time.Now().UnixNano()
resetAt := rl.resetAt.Load()
// Reset window jika sudah lewat
if now >= resetAt {
newResetAt := time.Now().Add(rl.window).UnixNano()
if rl.resetAt.CompareAndSwap(resetAt, newResetAt) {
rl.count.Store(0) // reset count
}
}
// Increment dan periksa limit
current := rl.count.Add(1)
return current <= rl.limit
}
// Penggunaan
limiter := NewRateLimiter(100, time.Second) // 100 req/detik
func handlerRateLimited(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
// proses request
}
Cache dengan Atomic Swap #
// Cache sederhana dengan atomic — tidak perlu lock saat membaca
type AtomicCache[K comparable, V any] struct {
mu sync.Mutex // hanya untuk write
data atomic.Pointer[map[K]V]
}
func NewAtomicCache[K comparable, V any]() *AtomicCache[K, V] {
c := &AtomicCache[K, V]{}
m := make(map[K]V)
c.data.Store(&m)
return c
}
// Get — tidak butuh lock, sangat cepat
func (c *AtomicCache[K, V]) Get(key K) (V, bool) {
m := c.data.Load()
v, ok := (*m)[key]
return v, ok
}
// Set — butuh lock untuk write, tapi buat copy baru agar Get tetap bebas lock
func (c *AtomicCache[K, V]) Set(key K, val V) {
c.mu.Lock()
defer c.mu.Unlock()
// Buat salinan map yang ada
old := c.data.Load()
baru := make(map[K]V, len(*old)+1)
for k, v := range *old {
baru[k] = v
}
baru[key] = val
// Swap atomik — pembaca yang sedang jalan mendapat map lama atau baru
// tapi keduanya konsisten (tidak ada map yang setengah-update)
c.data.Store(&baru)
}
// Penggunaan
cache := NewAtomicCache[string, *User]()
// Ribuan goroutine bisa Get bersamaan tanpa lock
go func() {
user, ok := cache.Get("budi")
if ok {
fmt.Println(user.Nama)
}
}()
// Write sesekali
cache.Set("budi", &User{Nama: "Budi"})
Kapan TIDAK Menggunakan atomic #
flowchart TD
Check{"Perlu operasi\npada lebih dari\nsatu variabel\nsecara konsisten?"}
Check -- Ya --> UseMutex["Gunakan sync.Mutex\natomic tidak cukup!"]
Check -- Tidak --> SimpleOp{"Operasi yang\ndiperlukan?"}
SimpleOp -- "Load / Store\nAdd / Swap\nCAS satu nilai" --> UseAtomic["Gunakan sync/atomic\nLebih cepat"]
SimpleOp -- "Logika yang lebih\nkompleks" --> UseMutex2["Gunakan sync.Mutex\nLebih aman"]
subgraph Bahaya["✗ Anti-Pattern — Jangan Lakukan"]
B1["Dua atomic ops yang\nharus konsisten bersamaan"]
B2["Baca-modifikasi-tulis\nbanyak variabel sekaligus"]
B3["Ganti Mutex dengan atomic\ntanpa memahami memory ordering"]
end
style UseMutex fill:#e3f2fd
style UseMutex2 fill:#e3f2fd
style UseAtomic fill:#e8f5e9
style Bahaya fill:#fce4ec// ANTI-PATTERN: dua atomic ops yang seharusnya satu unit
var sukses atomic.Int64
var gagal atomic.Int64
// Ini TIDAK atomik sebagai satu unit!
// Goroutine lain bisa membaca sukses dan gagal di antara dua operasi ini
sukses.Add(1)
// ← goroutine lain bisa snapshot di sini: sukses+1, gagal+0 (tidak konsisten!)
gagal.Add(-1)
// BENAR: gunakan Mutex untuk operasi yang harus konsisten
var mu sync.Mutex
var sukses2, gagal2 int64
mu.Lock()
sukses2++
gagal2--
mu.Unlock()
// ANTI-PATTERN: mencoba implementasi lock-free yang salah
var flag atomic.Bool
var data []byte
// Goroutine A:
flag.Store(true)
data = append(data, 1) // ini TIDAK dijamin terlihat setelah flag.Store!
// Goroutine B:
if flag.Load() {
fmt.Println(data) // mungkin melihat data kosong!
}
// Memory ordering lebih kompleks dari ini — gunakan channel atau Mutex!
Kapan Beralih ke Alternatif #
Tetap gunakan sync/atomic jika:
✓ Counter tunggal: request count, error count, hit count
✓ Flag boolean: shutdown, initialized, running
✓ Pointer yang diganti secara atomik: hot-reload config
✓ State machine sederhana dengan CAS
✓ Kode yang sangat performance-critical dengan profiling
Gunakan sync.Mutex jika:
✗ Perlu sinkronisasi beberapa variabel sekaligus
✗ Logika yang lebih kompleks dari Load/Store/Add/CAS
✗ Tidak yakin apakah atomic cukup — Mutex lebih aman
Gunakan sync.RWMutex jika:
✗ Banyak reader, sedikit writer, data lebih dari satu nilai
✗ Operasi baca membutuhkan membaca beberapa field sekaligus
Gunakan channel jika:
✗ Komunikasi antar goroutine (kirim data)
✗ Pipeline processing
✗ Fan-out / fan-in
✗ "Share memory by communicating" lebih natural
Pertimbangkan sync/atomic.Value untuk:
✗ Menyimpan nilai apapun (struct, slice, map) secara atomik
✗ Hot-reload konfigurasi yang sering dibaca
✗ Copy-on-write data structure
Ringkasan #
- Gunakan tipe baru di Go 1.19+:
atomic.Int64,atomic.Bool,atomic.Pointer[T]— lebih ergonomis, lebih aman dari tipe assertion, dan tidak butuh pointer manual.- atomic jauh lebih cepat dari Mutex untuk operasi tunggal (~5ns vs ~25ns), tapi hanya untuk operasi yang memang bisa dilakukan secara atomik — jangan paksa jika tidak cocok.
- atomic untuk satu nilai, Mutex untuk banyak nilai yang harus konsisten — ini adalah aturan paling penting. Dua operasi atomik berturut-turut BUKAN satu operasi atomik.
atomic.Valueuntuk hot-reload konfigurasi — Store sekali, Load ribuan kali tanpa lock. Tipe yang disimpan harus konsisten dan tidak boleh nil interface.- CAS (CompareAndSwap) untuk state machine — hanya izinkan transisi state yang valid dengan menjamin state saat ini sesuai yang diharapkan.
- Jangan gunakan atomic untuk menggantikan Mutex tanpa memahami memory ordering — ini adalah sumber bug yang sangat halus dan sulit di-debug.
atomic.Int64.Add(-1)untuk decrement — tidak ada operasi Subtract, gunakan Add dengan nilai negatif.- Selalu ukur dengan benchmark sebelum mengganti Mutex dengan atomic — premature optimization adalah akar dari semua kejahatan, dan atomic yang salah lebih buruk dari Mutex yang benar.
go test -racemasih diperlukan bahkan dengan atomic — race detector bisa mendeteksi penggunaan atomic yang tidak tepat.
← Sebelumnya: Flag