Sampai chapter ini kita diam-diam menikmati jaminan yang bagus: setelah join(), hasil kerja thread pasti terlihat; setelah kunci dilepas dan diambil thread lain, perubahan pasti terbaca. Jaminan itu bukan kebetulan — ia dirumuskan secara resmi dalam model memori C++. Model ini menjawab pertanyaan yang tak terjawab oleh atomisitas semata: kapan efek penulisan satu thread dijamin terlihat oleh thread lain, dan dalam urutan apa. Chapter ini membahasnya secara konseptual dan ringan — cukup untuk membaca kode konkuren dengan percaya diri — lalu membuktikan tiga memory_order yang utama lewat satu program teruji yang keluarannya deterministik.

Kenapa Butuh Aturan: Reordering #

Intuisi “kode berjalan urut dari atas ke bawah” berlaku untuk satu thread, tetapi tidak untuk yang lain yang sedang mengamatinya. Compiler boleh mengatur ulang instruksi demi kecepatan, dan CPU modern mengalirkan serta menulis-ulang operasi memori (out-of-order execution, store buffer). Selama hasilnya tidak berubah dari sudut pandang thread yang menjalankan kode, pengaturan ulang itu sah. Masalahnya muncul saat thread lain membaca data yang sama: tanpa aturan, ia bisa melihat efek yang bolong-bolong — misalnya sinyal “data siap” terlihat, tapi isinya belum.

Model memori C++ menutup celah itu dengan relasi happens-before: bila efek A happens-before efek B, maka A dijamin terlihat lengkap sebelum B. Sinkronisasi — join(), kunci mutex, atau pasangan memory_order yang tepat — adalah cara kita membuat relasi itu. Tanpa sinkronisasi apa pun antara dua thread yang menyentuh data sama, tidak ada happens-before, dan itulah definisi formal data race (dibahas tuntas di chapter berikutnya).

memory_order dalam Satu Pandangan #

Argumen opsional pada operasi std::atomic memilih kekuatan jaminan urutannya:

  • memory_order_seq_cst (default): paling kuat — semua operasi seq_cst terlihat oleh semua thread dalam SATU urutan global yang sama. Biaya terbesar, tapi paling mudah dinalar.
  • memory_order_acquire / memory_order_release: dipasangkan. Thread penulis melakukan store(release) — “semua penulisan sebelum baris ini boleh naik, tidak boleh turun melampaui baris ini”. Thread pembaca melakukan load(acquire) yang membaca nilai tersebut — dan sejak itu semua tulisan yang berada sebelum release-nya dijamin terlihat. Jembatan happens-before terbentuk antara kedua thread.
  • memory_order_relaxed: hanya menjamin atomisitas — tidak ada jaminan urutan terhadap operasi memori lain. Cocok untuk penghitung statistik yang isinya dilihat belakangan, bukan untuk mengirim data.
  • memory_order_acq_rel: gabungan acquire+release, relevan untuk operasi baca-tulis seperti fetch_add.
  • memory_order_consume: ada di standar, namun penerapannya direlaksasi praktis ke acquire — kita tidak memakainya.

Demo: default, relaxed, dan release-acquire #

Program teruji berikut menjalankan tiga skenario. Dua skenario pertama menunjukkan bahwa hasil AKHIR penghitung tetap benar di seq_cst maupun relaxed — atomisitas tidak bergantung pada memory_order. Skenario ketiga adalah inti chapter ini: mengirim data antar thread lewat pasangan release-acquire, tanpa mutex.

cpp
#include <atomic>
#include <iostream>
#include <thread>

int main() {
    // 1) Urutan default (seq_cst): penambahan oleh dua thread
    std::atomic<int> penghitung{0};
    auto tambah = [&penghitung] {
        for (int i = 0; i < 100000; ++i) {
            penghitung.fetch_add(1);   // default: memory_order_seq_cst
        }
    };
    std::thread t1(tambah), t2(tambah);
    t1.join(); t2.join();
    std::cout << "seq_cst (default) : " << penghitung.load() << "\n";

    // 2) relaxed: atomisitas tetap terjaga, urutan global tidak dijamin
    std::atomic<int> hitungRelaxed{0};
    auto tambahRelaxed = [&hitungRelaxed] {
        for (int i = 0; i < 100000; ++i) {
            hitungRelaxed.fetch_add(1, std::memory_order_relaxed);
        }
    };
    std::thread t3(tambahRelaxed), t4(tambahRelaxed);
    t3.join(); t4.join();
    std::cout << "relaxed           : " << hitungRelaxed.load() << "\n";

    // 3) release-acquire: mengirim data antar thread lewat satu flag
    std::atomic<bool> siap{false};
    int data = 0;      // variabel biasa, bukan atomic
    int bacaan = 0;

    std::thread produsen([&] {
        data = 42;                                     // 1) tulis data biasa
        siap.store(true, std::memory_order_release);   // 2) publikasikan
    });

    std::thread konsumen([&] {
        while (!siap.load(std::memory_order_acquire)) {   // 3) menunggu sinyal
            std::this_thread::yield();
        }
        bacaan = data;   // 4) dijamin melihat data == 42
    });

    produsen.join();
    konsumen.join();
    std::cout << "release-acquire   : data=" << data
              << ", bacaan=" << bacaan << "\n";
    return 0;
}

Output yang Dihasilkan:

text
seq_cst (default) : 200000
relaxed           : 200000
release-acquire   : data=42, bacaan=42

Bedah bagian ketiga — jantung dari chapter ini:

  1. Baris 34 (data = 42;): penulisan ke variabel biasa, non-atomik. Pada CPU nyata, tulisan ini bisa saja “tertinggal” di store buffer dan belum terlihat oleh thread lain.
  2. Baris 35 (store(true, release)): mempublikasikan sinyal. Aturan release: semua penulisan SEBELUM baris ini (termasuk data = 42) tidak boleh bergeser ke SETELAH baris ini. Sinyal jadi batas.
  3. Baris 39 (load(acquire)): konsumen berputar menunggu sinyal. Saat ia membaca true dengan acquire, terbentuklah happens-before: semua efek sebelum release produsen kini terlihat penuh bagi konsumen.
  4. Baris 42 (bacaan = data;): pembacaan ini dijamin mendapat 42 — bukan 0, bukan nilai setengah jadi. Inilah kemenangan release-acquire: data biasa dikirim antar thread tanpa mutex, cukup satu flag atomik.
  5. Baris 45-46 (join()): main() membaca data dan bacaan hanya setelah keduanya di-join() — sinkronisasi join memberi happens-before tersendiri, sehingga pencetakan aman dan output deterministik.

Catatan tentang baris relaxed : 200000: atomisitas fetch_add tetap berdiri di semua memory_order, jadi tidak ada penambahan yang hilang. Yang RELAXED tidak beri adalah urutan — bila relaxed dipakai untuk mempublikasikan data (bukan counter), konsumen bisa melihat sinyal true sambil membaca data yang masih 0.

Untuk penggunaan harian, rekomendasi praktisnya bertingkat: mulai dari default seq_cst; turun ke acquire/release bila profil menunjukkan biaya sinkronisasi yang nyata; sentuh relaxed hanya dengan alasan ukur yang jelas. Dan satu pelajaran yang harus melekat sebelum menulis kode konkuren yang lebih kompleks: bila dua thread menyentuh data yang sama TANPA salah satu mekanisme happens-before ini — bukan atomic, bukan mutex, bukan join — kita sudah berdiri di atas data race, topik penuh chapter berikutnya.

Referensi: cppreference — std::memory_order , cppreference — C++ memory model .