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.
/* 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:
gcc -std=c11 -Wall -Wextra -pthread -o atomic_counter atomic_counter.c
./atomic_counter && ./atomic_counter && ./atomic_counterOutput yang Dihasilkan:
Ekspektasi: 3000000
Aktual : 3000000
Ekspektasi: 3000000
Aktual : 3000000
Ekspektasi: 3000000
Aktual : 3000000Mari kita telaah baris-baris kode di atas:
- Baris 5 (
#include <stdatomic.h>): Header standar C11 yang mendefinisikan tipe_Atomic, macroatomic_*, dan tipe generikatomic_int,atomic_long,atomic_size_t. Tidak ada header POSIX yang dibutuhkan untuk atomics murni—ia bagian dari standar C. - Baris 7 (
static _Atomic long counter = 0;): Qualifier_Atomicmemberi tahu compiler bahwa setiap operasi padacounterharus atomic. Compiler akan memilih instruksi mesin yang tepat: pada x86_64,counter++menjadi instruksilock incqyang menjamin atomicity di tingkat CPU. - 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. - 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.
/* 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:
gcc -std=c11 -Wall -Wextra -o atomic_ops atomic_ops.c && ./atomic_opsOutput yang Dihasilkan:
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 = 47Mari kita telaah operasi-operasi di atas:
atomic_store(&x, 10): Tulis nilai ke variabel atomik. Berbeda dari assignment biasa (x = 10),atomic_storememberi memory order defaultmemory_order_seq_cst(sequentially consistent) yang paling kuat—semua thread melihat urutan operasi yang sama.atomic_load(&x): Baca nilai atomik. Tanpaatomic_load, pembacaan langsung (int v = x;) masih valid untuk tipe_Atomic, tetapi eksplisit lebih jelas dan memungkinkan kontrol memory order.atomic_exchange(&x, 25): Tulis nilai baru sambil mengembalikan nilai lama secara atomic. Berguna untuk mengimplementasi spinlock:while (atomic_exchange(&lock, 1) != 0).atomic_compare_exchange_strong(&x, &expected, desired): Inilah operasi paling kuat dalam atomics—Compare-And-Swap (CAS). Bila nilaixsama denganexpected, tulisdesireddan kembalikantrue. Bila tidak, isiexpecteddengan nilai aktual dan kembalikanfalse. CAS adalah blok bangunan universal untuk struktur data lock-free (queue, stack, hash map).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.
/* 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:
gcc -std=c11 -Wall -Wextra -pthread -O2 -o atomic_benchmark atomic_benchmark.c \
&& ./atomic_benchmarkOutput yang Dihasilkan (angka bergantung pada mesin):
Mutex : counter=4000000, waktu=0.456 detik
Atomic: counter=4000000, waktu=0.138 detikAtomics ~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.