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.

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

text
consumer: data=42

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-9 (static _Atomic int data = 0; static _Atomic bool ready = false;): dua variabel atomic. data adalah payload yang ditulis produsen, ready adalah flag yang menandakan data sudah siap.
  2. Baris 13 (atomic_store_explicit(&data, 42, memory_order_relaxed);): tulis data dengan ordering relaxed—cukup atomicity, tidak ada sinkronisasi.
  3. Baris 14 (atomic_store_explicit(&ready, true, memory_order_release);): tulis ready = true dengan release. Instruksi ini memastikan bahwa semua operasi memori sebelumnya (termasuk data = 42 di baris 13) terlihat oleh thread lain yang melakukan acquire pada ready.
  4. Baris 20 (while (!atomic_load_explicit(&ready, memory_order_acquire))): konsumen menunggu dengan acquire. Setelah acquire melihat ready == true, semua operasi memori sebelum release oleh produsen menjadi terlihat—termasuk data = 42.
  5. Baris 23 (atomic_load_explicit(&data, memory_order_relaxed);): baca data dengan relaxed. 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.

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

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

OrderKapan dipakai
memory_order_relaxedCounter 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_relRead-modify-write seperti fetch_add pada lock.
memory_order_seq_cstDefault. 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).

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