Apa itu operasi atomik?
Operasi atomik adalah operasi yang dijamin selesai secara utuh tanpa terputus oleh thread lain — tidak ada thread lain yang bisa melihat kondisi setengah jadi di tengah operasi tersebut. C11 menyediakan <stdatomic.h> yang memberi tipe dan fungsi atomik didukung langsung oleh hardware, tanpa perlu mutex sama sekali.
Ini beda mendasar dari mutex atau semaphore. Mutex melindungi beberapa baris kode (critical section) dengan lock eksplisit. Atomic membuat satu operasi tunggal, misalnya increment, selesai secara atomik di level instruksi CPU.
Ilustrasi
Tanpa atomic, counter++ sebenarnya 3 langkah: LOAD, ADD, STORE
Thread1: LOAD (0) → ADD (1) → STORE (1)
Thread2: LOAD (0) → ADD (1) → STORE (1) ← menimpa hasil Thread1
Hasil: 1 (salah, seharusnya 2)
Dengan atomic_fetch_add, tiap thread menjalankan LOAD-ADD-STORE
sebagai satu blok tak terputus. Hasil: 2 (benar)
Kenapa diperlukan
Untuk operasi sederhana seperti increment counter atau flag boolean, mutex sebenarnya agak berat — ada overhead lock/unlock padahal yang dilindungi cuma satu operasi kecil. Hardware modern punya instruksi khusus (seperti LOCK XADD di x86) yang bisa melakukan baca-ubah-tulis dalam satu langkah atomik, tanpa perlu lock software sama sekali. stdatomic.h memberi akses ke instruksi-instruksi ini secara portable, tanpa perlu inline assembly.
Implementasi
Struktur proyek
.
└── atomic_wallet.c
Step 1: wallet tanpa mutex
Contoh wallet yang sama seperti post mutex, tapi kali ini pakai atomic_int — tidak ada mutex sama sekali.
#include <stdatomic.h>
#include <pthread.h>
#include <stdio.h>
atomic_int saldo = 0;
void *deposit(void *arg)
{
int nominal = *(int *)arg;
atomic_fetch_add(&saldo, nominal); // baca-tambah-tulis, satu operasi tak terputus
return NULL;
}
int main(void)
{
pthread_t t1, t2;
int d1 = 300, d2 = 500;
pthread_create(&t1, NULL, deposit, &d1);
pthread_create(&t2, NULL, deposit, &d2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Saldo akhir: %d (expected: 800)\n", atomic_load(&saldo));
return 0;
}
Dibandingkan versi mutex, tidak ada pthread_mutex_init, lock, unlock, atau destroy sama sekali — cukup satu baris atomic_fetch_add.
Step 2: compare-and-swap
Untuk operasi yang lebih kompleks dari sekadar tambah, dasar dari kebanyakan struktur data lock-free adalah compare-and-swap.
#include <stdatomic.h>
int cas_increment(atomic_int *target)
{
int lama = atomic_load(target);
int baru;
do {
baru = lama + 1;
// kalau *target masih == lama, set ke baru dan berhenti;
// kalau tidak, lama di-update dengan nilai terbaru, ulangi
} while (!atomic_compare_exchange_weak(target, &lama, baru));
return baru;
}
Penjelasan
atomic_fetch_add membaca nilai saat ini, menambahkan nominal, dan menyimpannya kembali sebagai satu operasi atomik. Mengembalikan nilai sebelum ditambah, berguna kalau butuh tahu nilai lama.
atomic_load dan atomic_store membaca/menulis nilai atomic variable secara aman, memastikan tidak ada thread lain yang sedang menulis di tengah pembacaan.
atomic_compare_exchange_weak(target, &lama, baru) membandingkan *target dengan lama. Kalau sama, set *target = baru dan return true. Kalau berbeda karena diubah thread lain duluan, lama di-update jadi nilai terbaru dari *target, dan return false — makanya dipakai dalam loop do-while seperti contoh di atas. Versi _weak bisa gagal secara spurious walau nilainya sebenarnya cocok, sehingga harus dalam loop. Versi _strong tidak spurious-fail tapi bisa sedikit lebih mahal di beberapa arsitektur.
Soal memory ordering: semua fungsi di atas secara default pakai memory_order_seq_cst, ordering paling ketat dan paling gampang dipahami — semua thread melihat operasi atomik terjadi dalam urutan global yang sama. Ada ordering lain (relaxed, acquire, release) untuk optimisasi performa di kasus lanjutan, tapi itu topik tersendiri yang cukup dalam.
Timeline eksekusi
| Time | Thread1 (atomic_fetch_add) | Thread2 (atomic_fetch_add) | Saldo |
|---|---|---|---|
| t0 | fetch_add(300), atomik | (menunggu instruksi CPU) | 0 |
| t1 | selesai | fetch_add(500), atomik | 300 |
| t2 | selesai | 800 |
Tidak ada lock eksplisit — instruksi CPU itu sendiri yang menjamin atomisitas.
Kompilasi
gcc -o atomic_wallet atomic_wallet.c -lpthread
./atomic_wallet
Output:
Saldo akhir: 800 (expected: 800)
Best practices
Atomic cuma untuk operasi tunggal. Kalau logikanya “baca A, baca B, lalu tulis berdasarkan kondisi gabungan keduanya”, tetap butuh mutex atau CAS-loop yang benar, bukan sekadar mengganti tipe jadi atomic. compare_exchange_weak harus dalam loop karena bisa gagal secara spurious. Mulai dari memory_order_seq_cst — paling aman dan mudah dinalar, optimisasi ke ordering lain hanya kalau sudah profiling dan benar-benar butuh.
Waspadai ABA problem1: nilai bisa berubah dari A ke B lalu balik ke A lagi, dan CAS tidak mendeteksi perubahan itu karena nilai akhirnya sama. Dan jangan asal ganti semua mutex jadi atomic — atomic bagus untuk counter atau flag sederhana, bukan pengganti universal untuk semua critical section.
Kesimpulan
Operasi atomik memberi cara untuk melakukan operasi sederhana secara thread-safe tanpa lock sama sekali, memanfaatkan instruksi khusus di level hardware. Bagus untuk operasi tunggal seperti counter, dan jadi dasar untuk struktur data lock-free — tapi bukan pengganti mutex untuk logika majemuk.
Footnotes
-
ABA problem adalah salah satu jebakan klasik di struktur data lock-free berbasis CAS. Solusi umum termasuk menambahkan versioning atau tagged pointer pada nilai yang di-CAS. ↩