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 melakukanstore(release)— “semua penulisan sebelum baris ini boleh naik, tidak boleh turun melampaui baris ini”. Thread pembaca melakukanload(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 sepertifetch_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.
#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:
seq_cst (default) : 200000
relaxed : 200000
release-acquire : data=42, bacaan=42Bedah bagian ketiga — jantung dari chapter ini:
- 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. - Baris 35 (
store(true, release)): mempublikasikan sinyal. Aturan release: semua penulisan SEBELUM baris ini (termasukdata = 42) tidak boleh bergeser ke SETELAH baris ini. Sinyal jadi batas. - Baris 39 (
load(acquire)): konsumen berputar menunggu sinyal. Saat ia membacatruedengan acquire, terbentuklah happens-before: semua efek sebelum release produsen kini terlihat penuh bagi konsumen. - 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. - Baris 45-46 (
join()):main()membacadatadanbacaanhanya 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 .