Seluruh perangkat section ini — thread, kunci, atomic, kanal future — akan diuji sejatinya saat program berperilaku aneh dan kita harus mencari penyebabnya. Debugging program konkuren berbeda karakternya dari debugging biasa: bugnya bergantung pada waktu, sering hilang persis saat debugger menyala, dan tidak bisa direproduksi dengan menyalin langkah yang sama. Kabar baiknya, disiplin tertentu memperkecil wilayah pencarian secara drastis. Chapter penutup ini merangkum cara berpikirnya, membawa satu program teruji yang memakai logging disiplin dan uji invariant, lalu menutup section dengan rangkuman desain yang aman.

Kenapa Bug Konkuren Berbeda #

Dua sifat membuat bug konkuren sulit ditangkap. Pertama, nondeterminisme: urutan penjadwalan thread tidak pernah sama persis antar eksekusi, sehingga jalur yang memunculkan bug bisa hanya terjadi satu dari ribuan kali. Kedua, efek pengamat — program yang dijalankan di bawah debugger berjalan lebih lambat dan urutan waktunya bergeser, sehingga bug yang selalu muncul saat berdiri sendiri bisa tidak muncul saat diamati. Bug bertipe ini punya nama: heisenbug, kebalikan dari bug yang stabil dan mudah direproduksi.

Konsekuensi praktisnya: alat utama kita bukan stepping per baris, melainkan (1) penguat — membuat race lebih mudah muncul dengan stres berulang; (2) detektor — ThreadSanitizer yang membaca grafik happens-before langsung dari program; (3) pengurang — memperkecil program sampai penyebabnya telanjang; dan (4) bukti — invariant yang melanggar sendiri ketika keadaan internal salah.

Logging yang Disiplin #

Cetak-rawa (printf debugging) tetap berguna di dunia konkuren, TETAPI hanya bila diberi disiplin. Dua aturannya: setiap pesan adalah satu baris utuh yang ditulis di bawah satu mutex (agar baris tidak terpotong antar thread), dan setiap pesan menyebut siapa pelakunya dengan label tetap — bukan ID mentah yang berubah-ubah. Program berikut mendemonstrasikannya:

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

int main() {
    std::atomic<int> diproses{0};
    const int TARGET = 200;   // 100 dari pekerja A + 100 dari pekerja B

    // Log per-thread: setiap pekerja mencatat ke wadah miliknya sendiri
    // tanpa kunci dan tanpa data race; pencetakan dilakukan main()
    // setelah semua pekerja bergabung
    std::vector<std::string> logA, logB;
    auto kerja = [&](std::vector<std::string>& log, char nama) {
        log.push_back(std::string("[") + nama + "] mulai");
        for (int i = 0; i < 100; ++i) {
            diproses.fetch_add(1);
        }
        log.push_back(std::string("[") + nama + "] selesai");
    };

    std::thread pekerjaA(kerja, std::ref(logA), 'A');
    std::thread pekerjaB(kerja, std::ref(logB), 'B');

    pekerjaA.join();
    pekerjaB.join();

    // Semua pekerja selesai -> log utuh, urutan cetak pasti
    for (const std::string& baris : logA) {
        std::cout << baris << "\n";
    }
    for (const std::string& baris : logB) {
        std::cout << baris << "\n";
    }

    // Uji invariant: hasil akhir harus tepat TARGET
    if (diproses.load() != TARGET) {
        std::cout << "INVARIAN PELANGGAR: " << diproses.load() << "\n";
        return 1;
    }
    std::cout << "invariant terpenuhi: " << diproses.load() << "/"
              << TARGET << "\n";
    return 0;
}#include <atomic>
#include <iostream>
#include <string>
#include <thread>
#include <vector>

int main() {
    std::atomic<int> diproses{0};
    const int TARGET = 200;   // 100 dari pekerja A + 100 dari pekerja B

    // Log per-thread: setiap pekerja mencatat ke wadah miliknya sendiri
    // tanpa kunci dan tanpa data race; pencetakan dilakukan main()
    // setelah semua pekerja bergabung
    std::vector<std::string> logA, logB;
    auto kerja = [&](std::vector<std::string>& log, char nama) {
        log.push_back(std::string("[") + nama + "] mulai");
        for (int i = 0; i < 100; ++i) {
            diproses.fetch_add(1);
        }
        log.push_back(std::string("[") + nama + "] selesai");
    };

    std::thread pekerjaA(kerja, std::ref(logA), 'A');
    std::thread pekerjaB(kerja, std::ref(logB), 'B');

    pekerjaA.join();
    pekerjaB.join();

    // Semua pekerja selesai -> log utuh, urutan cetak pasti
    for (const std::string& baris : logA) {
        std::cout << baris << "\n";
    }
    for (const std::string& baris : logB) {
        std::cout << baris << "\n";
    }

    // Uji invariant: hasil akhir harus tepat TARGET
    if (diproses.load() != TARGET) {
        std::cout << "INVARIAN PELANGGAR: " << diproses.load() << "\n";
        return 1;
    }
    std::cout << "invariant terpenuhi: " << diproses.load() << "/"
              << TARGET << "\n";
    return 0;
}[A] mulai
[A] selesai
[B] mulai
[B] selesai
invariant terpenuhi: 200/200[main] program dimulai
[B] mulai
[B] selesai
[A] mulai
[A] selesai
[main] invariant terpenuhi: 200/200
[main] program berakhir

**PENTING**: keluaran di atas hanya representatif — urutan empat baris `[A]`/`[B]` bisa berbeda pada setiap eksekusi, karena kedua pekerja saling berlomba mencetak. Yang TIDAK berubah: baris `[main] program dimulai` selalu pertama (dicetak sebelum thread dibuat), dan tiga baris terakhir selalu di posisi itu (dicetak setelah kedua `join()`). Justru perbedaan urutan antar eksekusi inilah informasi debugging yang berharga — bila suatu eksekusi menampilkan pola yang tidak pernah terlihat, catat; itu petunjuk jalur terseling yang jarang.

Bedah barisnya:

1. **Baris 11-12 (log di bawah kunci)**: mutex menjamin satu baris log selalu utuh. Tanpa kunci, dua panggilan `cout` bisa berseling di tengah baris dan log menjadi tidak terbaca.
2. **Baris 16 (`std::atomic<int> diproses`)**: penghitung bersama yang dibesarkan kedua pekerja — atomic membuatnya bebas data race tanpa kunci tambahan.
3. **Baris 22 dan 30 (label tetap)**: `[A]` dan `[B]` adalah identitas yang stabil antar eksekusi, jauh lebih mudah dilacak daripada ID thread yang bentuknya berubah-ubah.
4. **Baris 37-38 (`join()`)**: batas yang membuat log setelahnya bisa dipercaya — apa pun yang tercetak setelah join pasti sudah terjadi.
5. **Baris 41-44 (uji invariant)**: `diproses` harus tepat 200. Bila penyebab lain mengacaukan hitungan, program melapor dan keluar dengan kode 1 — kegagalan yang terdeteksi sendiri, bukan yang harus dicari manual.

> [!NOTE]
> Jangan lupa quirk lingkungan: tampilan terminal di beberapa sistem bisa menyembunyikan bagian awal baris yang diawali kurung siku (misalnya `[m` pada `[main]`). Selalu verifikasi log lewat file (redirect ke file, baca filenya) — byte yang tersimpan adalah kebenarannya.

## Menyempitkan Lingkar Reproduksi

Bila program besar menunjukkan hasil yang salah secara sporadis, langkah berikutnya adalah reduksi: potong bagian program yang tidak relevan sampai tersisa kerangka kecil yang masih menunjukkan masalah. Buang satu modul, jalankan stres puluhan kali; masih muncul? modul itu memang tak bersalah. Tidak muncul lagi? penyebabnya barusan dibuang. Dengan binary search semacam ini, program ribuan baris bisa menyusut ke puluhan baris dalam satu sesi.

Satu hal yang perlu dijelaskan secara jujur: C++ tidak menyediakan "seed penjadwalan" — tidak ada cara standar untuk memutar ulang eksekusi dengan urutan thread yang sama persis, seperti seed pada generator angka acak. Yang bisa kita kendalikan adalah memperbesar peluang terseling: menambah `std::this_thread::yield()` di titik-titik kritis, menjalankan stres berulang di loop, dan membiarkan ThreadSanitizer memantau tiap eksekusi. Kombinasi stres + TSan + reduksi hampir selalu mengantarkan kita ke baris penyebab.

## Checklist Desain Program Konkuren

Daripada mengandalkan debugging, desain yang benar sejak awal menghemat semua sesi itu. Rangkuman section ini dalam satu checklist:

- **Segelisasi dulu**: data milik satu thread tidak butuh kunci; bagikan hanya bila memang perlu.
- **Banyak pembaca tanpa penulis aman; yang bercampur tulis harus dilindungi** — mutex, atomic, atau happens-before dari `join()`.
- **Satu mutex untuk satu kelompok data; critical section sependek mungkin.**
- **Beberapa mutex? selalu urutan sama, atau `scoped_lock`.**
- **Counter dan flag sederhana: `std::atomic`; pengiriman hasil: `future`; sinyal kerja: `condition_variable` dengan predicate.**
- **Hasil dikumpulkan lalu dicetak setelah join — output deterministik itu hadiah gratis dari disiplin desain.**
- **Semua program konkuren diuji di bawah `-fsanitize=thread` sebelum dipercaya.**

## Penutup Section: Concurrency dan Parallelism

Section ini dimulai dari pertanyaan paling dasar — apa beda proses dan thread — dan berakhir di perangkat penuh: `std::thread` dan siklus hidupnya, kunci (`mutex`, `lock_guard`, `unique_lock`, `shared_mutex`) beserta pencegahan deadlock, penantian berbasis sinyal (`condition_variable`), operasi atomik dan model memori di baliknya, definisi serta deteksi data race, kanal hasil (`promise`/`future`, `async`), penghentian kooperatif (`jthread`/`stop_token`), partisi paralel manual, dan disiplin debugging. Satu benang merahnya konsisten: determinisme bukan kebetulan, melainkan hasil desain — batasi data bersama, sinkronisasi yang jelas, dan agregasi sebelum mencetak.

Ke depan, keahlian ini akan terasa manfaatnya saat kita menyentuh pekerjaan yang memang berat secara hitungan. Section berikutnya, Pemrograman Numerik, membuka gerbang itu: mulai dari batas representasi angka di `int` dan `float` — topik yang, bila dihitung paralel nanti, harus benar SEBELUM dibagi ke banyak thread.

Referensi: [cppreference — Concurrency support library](https://en.cppreference.com/w/cpp/thread), [Instrumentation Options GCC (ThreadSanitizer)](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html).