Memory model C11 menentukan bagaimana operasi pada variabel atomic terlihat oleh thread lain. Tanpa memory model, compiler dan CPU bebas menyusun ulang urutan read/write untuk optimasi—dan urutan yang berbeda dapat mengubah perilaku program multi-thread. C11 menyediakan enam memory order mulai dari memory_order_relaxed (paling longgar) hingga memory_order_seq_cst (paling ketat). Pada chapter ini kita akan memahami tiga tingkat utama—relaxed, acquire/release, dan sequential consistency—dan kapan memakainya.
Tiga Tingkat Memory Ordering #
Standar C11 (ISO/IEC 9899:2011 §7.17.3) mendefinisikan enam konstanta memory order:
memory_order_relaxed: hanya atomicity, tidak ada ordering. Operasi dapat direorder bebas.memory_order_consume: bergantung data (jarang dipakai; biasanya dilakukan sebagai acquire).memory_order_acquire: read yang tidak boleh dipindahkan sebelum operasi memori berikutnya.memory_order_release: write yang tidak boleh dipindahkan setelah operasi memori sebelumnya.memory_order_acq_rel: kombinasi acquire dan release (untuk read-modify-write).memory_order_seq_cst: sequential consistency—default, paling ketat. Semua thread melihat urutan yang sama.
Pola produsen-konsumen klasik memakai acquire/release: produsen menulis data lalu menset flag ready dengan release. Konsumen menunggu flag dengan acquire, lalu membaca data dengan relaxed. Pasangan acquire/release menjamin bahwa data yang ditulis produsen sebelum release terlihat oleh konsumen setelah acquire.
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#include <stdbool.h>
/* Demo memory_order: produsen-konsumen sederhana */
static _Atomic int data = 0;
static _Atomic bool ready = false;
static void *producer(void *arg) {
(void)arg;
/* Tulis data dulu, lalu set ready dengan release */
atomic_store_explicit(&data, 42, memory_order_relaxed);
atomic_store_explicit(&ready, true, memory_order_release);
return NULL;
}
static void *consumer(void *arg) {
(void)arg;
/* Tunggu ready dengan acquire, lalu baca data */
while (!atomic_load_explicit(&ready, memory_order_acquire)) {
/* spin wait */
}
int v = atomic_load_explicit(&data, memory_order_relaxed);
printf("consumer: data=%d\n", v);
return NULL;
}
int main(void) {
pthread_t tp, tc;
pthread_create(&tp, NULL, producer, NULL);
pthread_create(&tc, NULL, consumer, NULL);
pthread_join(tp, NULL);
pthread_join(tc, NULL);
return 0;
}Output yang Dihasilkan:
consumer: data=42Mari kita telaah baris-baris kode di atas:
- Baris 8-9 (
static _Atomic int data = 0; static _Atomic bool ready = false;): dua variabel atomic.dataadalah payload yang ditulis produsen,readyadalah flag yang menandakan data sudah siap. - Baris 13 (
atomic_store_explicit(&data, 42, memory_order_relaxed);): tulis data dengan orderingrelaxed—cukup atomicity, tidak ada sinkronisasi. - Baris 14 (
atomic_store_explicit(&ready, true, memory_order_release);): tulisready = truedenganrelease. Instruksi ini memastikan bahwa semua operasi memori sebelumnya (termasukdata = 42di baris 13) terlihat oleh thread lain yang melakukanacquirepadaready. - Baris 20 (
while (!atomic_load_explicit(&ready, memory_order_acquire))): konsumen menunggu dengan acquire. Setelah acquire melihatready == true, semua operasi memori sebelum release oleh produsen menjadi terlihat—termasukdata = 42. - Baris 23 (
atomic_load_explicit(&data, memory_order_relaxed);): baca data denganrelaxed. Setelah acquire, kita sudah yakin data sudah tertulis—tidak perlu ordering tambahan untuk read ini.
Sequential Consistency: Default Paling Kuat #
memory_order_seq_cst adalah default untuk operasi atomic tanpa _explicit. Ordering ini memberi jaminan terkuat: semua thread melihat urutan operasi atomic yang sama, seolah-olah semua operasi dilakukan dalam urutan global yang konsisten.
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
/* Memory_order_seq_cst: default, paling kuat */
static _Atomic int x = 0;
static _Atomic int y = 0;
static int r1 = 0, r2 = 0;
static void *writer_x(void *arg) {
(void)arg;
atomic_store_explicit(&x, 1, memory_order_seq_cst);
r1 = atomic_load_explicit(&y, memory_order_seq_cst);
return NULL;
}
static void *writer_y(void *arg) {
(void)arg;
atomic_store_explicit(&y, 1, memory_order_seq_cst);
r2 = atomic_load_explicit(&x, memory_order_seq_cst);
return NULL;
}
int main(void) {
/* Run beberapa kali untuk menunjukkan tidak ada (0,0) */
int runs = 1000;
int both_zero = 0;
for (int i = 0; i < runs; i++) {
atomic_store(&x, 0);
atomic_store(&y, 0);
r1 = -1; r2 = -1;
pthread_t t1, t2;
pthread_create(&t1, NULL, writer_x, NULL);
pthread_create(&t2, NULL, writer_y, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
if (r1 == 0 && r2 == 0) both_zero++;
}
printf("seq_cst: %d/%d r1==0 r2==0 (harusnya 0)\n", both_zero, runs);
return 0;
}Output yang Dihasilkan:
seq_cst: 0/1000 r1==0 r2==0 (harusnya 0)Tes di atas adalah varian dari store-store litmus test. Dua thread masing-masing menulis satu variabel lalu membaca variabel lawan. Tanpa seq_cst (misalnya dengan relaxed), adalah mungkin r1 == 0 && r2 == 0—keduanya membaca 0 karena write masing-masing belum terlihat. Dengan seq_cst, hal ini dilarang: harus ada urutan global, sehingga salah satu write terjadi sebelum read lainnya.
Panduan Memilih Memory Order #
| Order | Kapan dipakai |
|---|---|
memory_order_relaxed | Counter yang tidak perlu ordering dengan operasi lain. Contoh: statistik thread-local yang diagregasi. |
memory_order_acquire (load) | Membaca flag yang dilepas dengan release. Contoh: konsumen menunggu produsen. |
memory_order_release (store) | Menulis flag setelah data siap. Contoh: produsen menset ready. |
memory_order_acq_rel | Read-modify-write seperti fetch_add pada lock. |
memory_order_seq_cst | Default. Multi-producer multi-consumer, atau bila ragu. |
Kompatibilitas Compiler #
Memory model C11 memerlukan compiler yang mendukung C11 atau lebih baru. GCC sejak 4.7, Clang sejak 3.1, dan MSVC 19.10 sudah mendukung <stdatomic.h>. Untuk kode lama, library libatomic mungkin diperlukan bila hardware tidak mendukung atomic native untuk tipe tertentu (misalnya int128_t).
gcc -std=c11 -Wall -Wextra -pthread -o test_mm1 test_mm1.c