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.

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

text
counter (target 200000): 200000
status: OK

Mari kita telaah baris-baris kode di atas:

  1. Baris 7 (static _Atomic int counter = 0;): deklarasi variabel atomic. _Atomic adalah type qualifier yang dapat dipakai dengan tipe apa pun: _Atomic int, _Atomic long, _Atomic void *. Compiler memastikan setiap akses adalah atomic.
  2. Baris 11 (atomic_fetch_add(&counter, 1);): operasi fetch-and-add yang atomic. Mengembalikan nilai lama, lalu menambah. Pada x86-64, diimplementasikan dengan instruksi lock xadd yang merupakan operasi atomic hardware.
  3. Baris 16-17 (pthread_create(&t1, NULL, worker, NULL);): membuat dua thread yang menjalankan worker. Kedua thread memanggil atomic_fetch_add pada counter yang sama—100.000 kali masing-masing.
  4. Baris 19-20 (pthread_join): menunggu kedua thread selesai. Setelah join, main dapat membaca counter dengan aman.
  5. Baris 22 (int final = atomic_load(&counter);): membaca nilai atomic. atomic_load menjamin 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.

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

text
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:

bash
gcc -std=c11 -Wall -Wextra -pthread -o test_at1 test_at1.c

Tanpa -pthread, linker akan gagal menemukan simbol pthread_create dan pthread_join.