Atomics adalah fitur C11 yang memungkinkan kita melakukan operasi pada variabel yang dibagi antar-thread tanpa data race. Tanpa atomics, operasi counter++ yang tampak sederhana sebenarnya terdiri dari tiga langkah: baca, tambah, dan tulis. Bila dua thread melakukan ini secara bersamaan, salah satu increment bisa hilang. _Atomic memberi jaminan bahwa operasi read-modify-write tidak dapat dipotong di tengah. Pada chapter ini kita akan membangun counter atomic dengan dua thread yang berjalan paralel, lalu mempelajari spinlock berbasis atomic_flag dan operasi compare-exchange.
Counter dengan _Atomic int #
Deklarasi _Atomic int counter memberi tahu compiler bahwa setiap akses ke counter harus dilakukan secara atomik. Operasi seperti atomic_fetch_add(&counter, 1) melakukan increment tanpa dapat dipotong oleh thread lain—dijamin konsisten oleh hardware.
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#define N_ITER 100000
static _Atomic int counter = 0;
static void *worker(void *arg) {
(void)arg;
for (int i = 0; i < N_ITER; i++) {
atomic_fetch_add(&counter, 1);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, NULL);
pthread_create(&t2, NULL, worker, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
int final = atomic_load(&counter);
printf("counter (target %d): %d\n", 2 * N_ITER, final);
printf("status: %s\n", final == 2 * N_ITER ? "OK" : "RACE");
return 0;
}Output yang Dihasilkan:
counter (target 200000): 200000
status: OKMari kita telaah baris-baris kode di atas:
- Baris 7 (
static _Atomic int counter = 0;): deklarasi variabel atomic._Atomicadalah type qualifier yang dapat dipakai dengan tipe apa pun:_Atomic int,_Atomic long,_Atomic void *. Compiler memastikan setiap akses adalah atomic. - Baris 11 (
atomic_fetch_add(&counter, 1);): operasi fetch-and-add yang atomic. Mengembalikan nilai lama, lalu menambah. Pada x86-64, diimplementasikan dengan instruksilock xaddyang merupakan operasi atomic hardware. - Baris 16-17 (
pthread_create(&t1, NULL, worker, NULL);): membuat dua thread yang menjalankanworker. Kedua thread memanggilatomic_fetch_addpadacounteryang sama—100.000 kali masing-masing. - Baris 19-20 (
pthread_join): menunggu kedua thread selesai. Setelah join,maindapat membacacounterdengan aman. - Baris 22 (
int final = atomic_load(&counter);): membaca nilai atomic.atomic_loadmenjamin bahwa bacaan tidak menyaksikan nilai parsial (torn read) yang mungkin terjadi pada tipe non-atomic di arsitektur tertentu.
Spinlock dengan atomic_flag dan Compare-Exchange #
atomic_flag adalah tipe atomic paling sederhana dengan dua operasi: test_and_set dan clear. Tipe ini cocok untuk membangun spinlock manual—lock sederhana yang menunggu dengan busy loop.
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#include <stdbool.h>
/* Atomic flag untuk lock sederhana (spinlock) */
static atomic_flag lock = ATOMIC_FLAG_INIT;
static int shared_data = 0;
void spinlock_acquire(atomic_flag *f) {
while (atomic_flag_test_and_set(f)) {
/* busy wait */
}
}
void spinlock_release(atomic_flag *f) {
atomic_flag_clear(f);
}
static void *worker(void *arg) {
int n = *(int *)arg;
for (int i = 0; i < n; i++) {
spinlock_acquire(&lock);
shared_data++;
spinlock_release(&lock);
}
return NULL;
}
/* Compare-exchange: update jika nilai cocok */
static void demo_cas(void) {
_Atomic int val = 10;
int expected = 10;
int desired = 20;
int ok = atomic_compare_exchange_strong(&val, &expected, desired);
printf("CAS pertama: ok=%d val=%d (expected=%d)\n", ok, val, expected);
/* CAS kedua: expected 99 (salah), val tidak berubah */
expected = 99;
desired = 30;
ok = atomic_compare_exchange_strong(&val, &expected, desired);
printf("CAS kedua: ok=%d val=%d (expected=%d)\n", ok, val, expected);
}
int main(void) {
pthread_t t1, t2;
int per_thread = 50000;
pthread_create(&t1, NULL, worker, &per_thread);
pthread_create(&t2, NULL, worker, &per_thread);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("shared_data (target %d): %d\n", 2 * per_thread, shared_data);
demo_cas();
return 0;
}Output yang Dihasilkan:
shared_data (target 100000): 100000
CAS pertama: ok=1 val=20 (expected=10)
CAS kedua: ok=0 val=20 (expected=20)Spinlock spinlock_acquire memakai atomic_flag_test_and_set yang menset flag ke true dan mengembalikan nilai lama. Bila nilai lama adalah true (sudah dipegang thread lain), kita loop menunggu. Bila false (bebas), kita mendapat lock. spinlock_release memakai atomic_flag_clear untuk melepas.
Fungsi demo_cas menunjukkan compare-and-swap (CAS), operasi fundamental dalam pemrograman lock-free. atomic_compare_exchange_strong(&val, &expected, desired) memeriksa apakah val == expected. Bila ya, val di-set ke desired dan mengembalikan true. Bila tidak, expected di-set ke nilai val saat ini dan mengembalikan false. Pada output, CAS pertama sukses (val 10 → 20), CAS kedua gagal karena val sudah 20, bukan 99.
Operasi Atomic Lainnya #
<stdatomic.h> menyediakan banyak operasi:
atomic_store(&x, val): tulis nilai.atomic_load(&x): baca nilai.atomic_exchange(&x, val): tulis nilai, kembalikan nilai lama.atomic_fetch_add,atomic_fetch_sub,atomic_fetch_or,atomic_fetch_and,atomic_fetch_xor: operasi aritmatika dan logika yang atomic.atomic_thread_fence(memory_order_seq_cst): barrier memori eksplisit.
Kompilasi dengan -pthread #
Atomics sendiri tidak memerlukan -pthread, tetapi contoh di atas memakai pthread_create untuk menjalankan thread. Selalu sertakan flag ini saat kompilasi:
gcc -std=c11 -Wall -Wextra -pthread -o test_at1 test_at1.cTanpa -pthread, linker akan gagal menemukan simbol pthread_create dan pthread_join.