Selama ini program yang kita tulis berjalan sebagai satu alur eksekusi: main() dieksekusi dari atas ke bawah, satu pernyataan demi satu pernyataan. Mulai section ini kita membuka dimensi baru — concurrency: kemampuan program menjalankan beberapa alur eksekusi sekaligus di dalam satu proses. Sebelum menyentuh API-nya, kita perlu membedakan dua konsep dasar yang sering tertukar: proses dan thread. Chapter ini membedah keduanya, menjelaskan mengapa thread di satu proses bisa berbagi memori secara langsung, lalu membuktikan lewat program teruji bahwa pembagian pekerjaan ke dua thread menghasilkan keluaran yang sama persis dengan versi satu thread.

Proses dan Thread: Apa Bedanya #

Proses adalah program yang sedang berjalan, lengkap dengan ruang memorinya sendiri: code, data, heap, dan stack. Ketika kita menjalankan dua salinan program yang sama, sistem operasi membuat dua proses yang saling terisolasi — satu proses tidak bisa membaca variabel milik proses lain secara langsung. Komunikasi antar proses butuh perantara seperti pipe, socket, atau file, karena isolasi itulah inti dari model proses.

Thread adalah unit eksekusi di dalam proses. Satu proses bisa memuat banyak thread, dan semua thread itu berbagi ruang memori proses yang sama: variabel global, heap, dan data terbuka bagi semuanya. Yang tidak dibagi adalah porsi “pribadi” tiap thread — terutama stack-nya sendiri serta register CPU yang dipakai saat menjalankan kode. Jadi dua thread bisa memanggil function yang sama bersamaan, tetapi masing-masing punya variabel lokal sendiri.

Perbandingan ringkasnya seperti ini:

AspekProsesThread
Ruang memoriTerisolasi, milik sendiriBerbagi memori proses
Biaya pembuatanBerat (alokasi lengkap)Ringan (cukup stack baru)
KomunikasiButuh perantara (pipe, socket)Langsung lewat variabel bersama
KegagalanSatu proses berhenti, lainnya amanThread yang keliru bisa merusak data bersama

Memori Bersama: Keunggulan sekaligus Jebakan #

Berbagi memori adalah alasan multithreading enak dipakai: tidak ada proses penyalinan data antar pihak. Thread A menulis ke std::vector, thread B bisa langsung membacanya tanpa salinan. Namun kebebasan ini bayar mahal bila tidak dikawal. Ketika dua thread menulis ke variabel yang sama tanpa aturan, hasilnya bergantung pada urutan kedatangan mereka — kondisi yang disebut data race, dan programnya bisa menghasilkan jawaban yang salah. Karena itu sebagian besar chapter di section ini berkutat pada satu tema: bagaimana membagi memori dengan tertib, lewat std::mutex, operasi atomik, dan desain yang sadar-konkurensi.

Ada aturan emas yang kita pegang sejak sekarang: data yang hanya disentuh satu thread aman; data yang dibaca banyak thread tanpa perubahan juga aman; yang berbahaya hanyalah data yang ditulis sambil dibaca atau ditulis oleh pihak lain pada saat bersamaan. Desain terbaik sering kali bukan yang menambah banyak kunci, melainkan yang meminimalkan data bersama itu sendiri.

Konteks Switching #

Sistem operasi menjadwalkan thread ke core CPU lewat scheduler. Bila jumlah thread melebihi jumlah core, scheduler berpindah-pindah antar thread ratusan kali per detik: menyimpan state eksekusi thread yang sedang jalan (nilai register, posisi kode) lalu memuat state thread berikutnya. Pergantian ini disebut konteks switching, dan biayanya nyata — tidak ada hasil hitung yang dihasilkan selama pergantian.

Dua konsekuensi praktisnya perlu kita ingat. Pertama, membuat lebih banyak thread daripada pekerjaan yang ada justru menambah overhead switching tanpa menambah produktivitas. Kedua, urutan kapan thread mendapat jatah CPU tidak bisa kita kendalikan dari kode — inilah sumber perilaku nondeterministik yang akan kita temui terus di section ini.

Kapan Multithreading Tepat #

Multithreading bukan obat untuk semua program yang lambat. Ia paling tepat untuk dua situasi: pekerjaan yang terdiri atas banyak sub-tugas independen (pemrosesan data, transformasi file, agregasi hitungan) dan program yang harus tetap responsif sambil mengerjakan sesuatu (thread UI tidak boleh terhalang thread pemrosesan). Sebaliknya, pekerjaan yang bergantung pada hasil langkah sebelumnya, atau yang bottleneck-nya ada di I/O disk dan jaringan, biasanya lebih cocok diselesaikan dengan pendekatan lain seperti asinkron I/O.

Berikut program teruji yang membandingkan dua pendekatan pada pekerjaan hitung yang bisa dipecah: menjumlahkan kuadrat 1 sampai 2.000.000. Versi satu thread mengerjakan seluruh rentang; versi dua thread membagi rentang jadi dua bagian, lalu menggabungkan hasilnya setelah kedua pekerja selesai.

cpp
#include <iostream>
#include <thread>

// Pekerjaan murni hitung: menjumlahkan kuadrat pada rentang [awal, akhir]
long long jumlahKuadrat(long long awal, long long akhir) {
    long long total = 0;
    for (long long i = awal; i <= akhir; ++i) {
        total += i * i;
    }
    return total;
}

int main() {
    const long long N = 2'000'000;

    // Versi satu thread: seluruh rentang dikerjakan berurutan
    long long totalSatu = jumlahKuadrat(1, N);

    // Versi dua thread: rentang dibagi dua, hasilnya digabung setelah join
    long long hasilA = 0;
    long long hasilB = 0;
    std::thread pekerjaA([&] { hasilA = jumlahKuadrat(1, N / 2); });
    std::thread pekerjaB([&] { hasilB = jumlahKuadrat(N / 2 + 1, N); });
    pekerjaA.join();   // tunggu pekerja A selesai
    pekerjaB.join();   // tunggu pekerja B selesai
    long long totalDua = hasilA + hasilB;

    std::cout << "Satu thread  : " << totalSatu << "\n";
    std::cout << "Dua thread   : " << totalDua << "\n";
    std::cout << "Hasil identik: " << std::boolalpha
              << (totalSatu == totalDua) << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Satu thread  : 2666668666667000000
Dua thread   : 2666668666667000000
Hasil identik: true

Bedah baris pentingnya:

  1. Baris 18 (long long totalSatu = ...): baseline — seluruh pekerjaan diselesaikan satu alur eksekusi, tanpa thread tambahan.
  2. Baris 23-24 (std::thread pekerjaA/pekerjaB): dua thread dibuat, masing-masing menerima lambda yang menghitung separuh rentang dan menyimpan hasilnya ke variabel berbeda. Dua variabel terpisah sengaja dipilih agar kedua thread tidak menyentuh data yang sama.
  3. Baris 25-26 (join()): main() berhenti di sini sampai kedua pekerja benar-benar selesai. Tanpa join(), program bisa membaca hasilA/hasilB saat isinya belum ditulis.
  4. Baris 27 (hasilA + hasilB): penggabungan dilakukan SETELAH kedua join() — inilah pola yang membuat keluaran multithread tetap deterministik: hasil dikumpulkan, baru dicetak.
  5. Baris 31-32 (pencetakan): kedua pendekatan menghasilkan angka yang sama persis, karena pembagian rentang 1..1.000.000 dan 1.000.001..2.000.000 tidak saling tumpang-tindih.

Perhatikan bahwa program ini sengaja tidak mengklaim versi dua thread lebih cepat: pada mesin dengan core terbatas, keuntungan kecepatan bisa kecil atau tidak ada, karena total kemampuan hitung CPU tetap sama. Yang kita buktikan di sini adalah kebenaran hasil — fondasi sebelum bicara performa.

Dengan pemahaman proses, thread, dan konteks switching ini, kita siap menyentuh API-nya. Chapter berikutnya memperkenalkan std::thread — cara standar membuat, menghidupi, dan mengakhiri thread di C++.

Referensi: cppreference — Concurrency support library , cppreference — std::thread .