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:#e3f2fd

API 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 atomic
import "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.Value untuk 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 -race masih diperlukan bahkan dengan atomic — race detector bisa mendeteksi penggunaan atomic yang tidak tepat.

← Sebelumnya: Flag
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact