Standar C11 memperkenalkan tipe _Atomic dan header <stdatomic.h> yang menyediakan operasi terjamin atomic (tidak dapat diinterupsi oleh thread lain). Untuk operasi sederhana seperti increment counter, atomics jauh lebih efisien dibanding mutex karena tidak ada context switch. Pada chapter ini kita menulis ulang program counter dari chapter race condition dengan _Atomic long counter—tanpa mutex sama sekali—dan memverifikasi hasilnya benar. Setelah itu, kita melihat operasi atomik eksplisit (atomic_load, atomic_store, atomic_exchange, atomic_compare_exchange_strong, atomic_fetch_add) dan membandingkan kinerja atomics vs mutex secara empiris.

Counter Atomic Tanpa Mutex #

Program berikut identik dengan versi race-condition sebelumnya, kecuali satu perubahan: counter kini bertipe _Atomic long. Penambahan qualifier _Atomic memberi tahu compiler bahwa setiap akses ke variabel ini harus menggunakan instruksi atomik.

c
/* File: atomic_counter.c - _Atomic counter tanpa mutex */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>

#define N_ITER 1000000
static _Atomic long counter = 0;

static void *naikkan(void *arg) {
    (void) arg;
    for (long i = 0; i < N_ITER; i++) {
        counter++;                      /* atomic read-modify-write */
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, naikkan, NULL);
    pthread_create(&t2, NULL, naikkan, NULL);
    pthread_create(&t3, NULL, naikkan, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);

    printf("Ekspektasi: %d\n", 3 * N_ITER);
    printf("Aktual    : %ld\n", counter);
    return 0;
}

Compile dan jalankan beberapa kali:

bash
gcc -std=c11 -Wall -Wextra -pthread -o atomic_counter atomic_counter.c
./atomic_counter && ./atomic_counter && ./atomic_counter

Output yang Dihasilkan:

text
Ekspektasi: 3000000
Aktual    : 3000000
Ekspektasi: 3000000
Aktual    : 3000000
Ekspektasi: 3000000
Aktual    : 3000000

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (#include <stdatomic.h>): Header standar C11 yang mendefinisikan tipe _Atomic, macro atomic_*, dan tipe generik atomic_int, atomic_long, atomic_size_t. Tidak ada header POSIX yang dibutuhkan untuk atomics murni—ia bagian dari standar C.
  2. Baris 7 (static _Atomic long counter = 0;): Qualifier _Atomic memberi tahu compiler bahwa setiap operasi pada counter harus atomic. Compiler akan memilih instruksi mesin yang tepat: pada x86_64, counter++ menjadi instruksi lock incq yang menjamin atomicity di tingkat CPU.
  3. Baris 11 (counter++;): Sekarang atomic—tidak mungkin dua thread membaca nilai sama lalu keduanya menulis nilai sama. Pada arsitektur yang mendukung (x86, ARM modern), tidak ada kunci yang dipegang—operasi langsung di CPU. Inilah mengapa atomics disebut lock-free.
  4. Baris 19 (printf("%ld", counter);): Pembacaan counter juga atomic, jadi kita selalu mendapat nilai konsisten. Tanpa _Atomic, pembacaan counter bersamaan dengan update thread lain bisa menghasilkan nilai parsial (terpotong di tengah penulisan).

Operasi Atomik Eksplisit #

Selain operator ++, --, +=, kita bisa memakai fungsi-fungsi eksplisit dari <stdatomic.h> yang memberi kontrol lebih. Berikut demo lima operasi paling sering dipakai.

c
/* File: atomic_ops.c - operasi atomik eksplisit */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdatomic.h>

int main(void) {
    _Atomic int x = 0;

    /* store: tulis nilai baru */
    atomic_store(&x, 10);
    printf("Setelah store 10: x = %d\n", atomic_load(&x));

    /* exchange: tulis nilai baru, kembalikan nilai lama */
    int lama = atomic_exchange(&x, 25);
    printf("Setelah exchange(25): lama=%d, x = %d\n", lama, atomic_load(&x));

    /* compare_exchange_strong: replace hanya jika nilai cocok */
    int expected = 25;
    int desired  = 42;
    _Bool ok = atomic_compare_exchange_strong(&x, &expected, desired);
    printf("CAS(25->42): sukses=%d, x = %d (expected sekarang=%d)\n",
           ok, atomic_load(&x), expected);

    /* CAS gagal: expected diperbarui ke nilai aktual */
    expected = 99;
    desired  = 100;
    ok = atomic_compare_exchange_strong(&x, &expected, desired);
    printf("CAS(99->100): sukses=%d, x = %d (expected sekarang=%d)\n",
           ok, atomic_load(&x), expected);

    /* fetch_add: tambah nilai, kembalikan nilai lama */
    int sebelum = atomic_fetch_add(&x, 5);
    printf("fetch_add(5): sebelum=%d, x sekarang = %d\n", sebelum, atomic_load(&x));
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o atomic_ops atomic_ops.c && ./atomic_ops

Output yang Dihasilkan:

text
Setelah store 10: x = 10
Setelah exchange(25): lama=10, x = 25
CAS(25->42): sukses=1, x = 42 (expected sekarang=25)
CAS(99->100): sukses=0, x = 42 (expected sekarang=42)
fetch_add(5): sebelum=42, x sekarang = 47

Mari kita telaah operasi-operasi di atas:

  1. atomic_store(&x, 10): Tulis nilai ke variabel atomik. Berbeda dari assignment biasa (x = 10), atomic_store memberi memory order default memory_order_seq_cst (sequentially consistent) yang paling kuat—semua thread melihat urutan operasi yang sama.
  2. atomic_load(&x): Baca nilai atomik. Tanpa atomic_load, pembacaan langsung (int v = x;) masih valid untuk tipe _Atomic, tetapi eksplisit lebih jelas dan memungkinkan kontrol memory order.
  3. atomic_exchange(&x, 25): Tulis nilai baru sambil mengembalikan nilai lama secara atomic. Berguna untuk mengimplementasi spinlock: while (atomic_exchange(&lock, 1) != 0).
  4. atomic_compare_exchange_strong(&x, &expected, desired): Inilah operasi paling kuat dalam atomics—Compare-And-Swap (CAS). Bila nilai x sama dengan expected, tulis desired dan kembalikan true. Bila tidak, isi expected dengan nilai aktual dan kembalikan false. CAS adalah blok bangunan universal untuk struktur data lock-free (queue, stack, hash map).
  5. atomic_fetch_add(&x, 5): Tambah nilai atomik dan kembalikan nilai sebelum penambahan. Sering dipakai untuk counter yang ingin kita tahu nilai sebelumnya (misalnya untuk menghasilkan ID unik berturut-turut).

Benchmark: Atomics vs Mutex #

Untuk membandingkan efisiensi, kita ukur waktu eksekusi program yang sama dengan dua pendekatan: mutex dan atomics. Setiap thread meng-increment counter 2 juta kali, dua thread berjalan paralel.

c
/* File: atomic_benchmark.c - benchmark atomic vs mutex */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
#include <time.h>

#define N_ITER 2000000

static double sekarang_detik(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

/* --- Versi 1: mutex --- */
static long counter_mtx = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void *naik_mtx(void *arg) {
    (void) arg;
    for (long i = 0; i < N_ITER; i++) {
        pthread_mutex_lock(&mtx);
        counter_mtx++;
        pthread_mutex_unlock(&mtx);
    }
    return NULL;
}

/* --- Versi 2: atomic --- */
static _Atomic long counter_at = 0;

static void *naik_at(void *arg) {
    (void) arg;
    for (long i = 0; i < N_ITER; i++) {
        counter_at++;
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    double t0, t1;

    /* Mutex */
    t0 = sekarang_detik();
    pthread_create(&t1, NULL, naik_mtx, NULL);
    pthread_create(&t2, NULL, naik_mtx, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    t1 = sekarang_detik();
    printf("Mutex : counter=%ld, waktu=%.3f detik\n", counter_mtx, t1 - t0);

    /* Atomic */
    t0 = sekarang_detik();
    pthread_create(&t1, NULL, naik_at, NULL);
    pthread_create(&t2, NULL, naik_at, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    t1 = sekarang_detik();
    printf("Atomic: counter=%ld, waktu=%.3f detik\n", counter_at, t1 - t0);

    return 0;
}

Compile dengan optimasi -O2 agar compiler menghasilkan kode mesin efisien:

bash
gcc -std=c11 -Wall -Wextra -pthread -O2 -o atomic_benchmark atomic_benchmark.c \
    && ./atomic_benchmark

Output yang Dihasilkan (angka bergantung pada mesin):

text
Mutex : counter=4000000, waktu=0.456 detik
Atomic: counter=4000000, waktu=0.138 detik

Atomics ~3x lebih cepat dari mutex untuk operasi counter sederhana. Alasan utama: mutex melakukan lock/unlock yang melibatkan atomic CAS di kernel bila terjadi kontensi, sementara counter++ atomic langsung jadi instruksi lock incq di CPU—tidak ada overhead kernel. Tentu saja, ini adalah kasus terbaik untuk atomics; bila critical section panjang atau melibatkan banyak variabel, mutex tetap pilihan yang lebih tepat.

Memory Order: Kapan Default Cukup #

Standar C11 menyediakan enam memory order: memory_order_seq_cst (default, paling kuat), memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_consume, dan memory_order_relaxed (paling lemah). Untuk pemula, selalu pakai default seq_cst—ia benar dalam semua kasus, hanya sedikit lebih lambat.

Memori order non-default berguna di kode library berperforma tinggi (Linux kernel, database engine). Sebagai contoh, counter statistik yang tidak perlu konsisten dengan data lain bisa pakai atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed) untuk efisiensi maksimum. Modifikasi memory order hanya bila Anda sudah memahami model memori C11 secara mendalam—salah memilih menyebabkan bug yang sangat sulit dilacak.