std::thread adalah pintu masuk utama kita ke dunia multithreading di C++: objek yang mewakili satu alur eksekusi yang sedang berjalan. Membuatnya sederhana — beri tugas berupa fungsi atau lambda, dan thread mulai berjalan seketika. Namun di balik kesederhanaan itu ada aturan main yang tidak bisa ditawar: setiap thread harus di-join() (atau dengan sengaja dilepas), argumen disalin secara default sehingga mengirim referensi butuh std::ref, dan thread yang di-destruct saat masih “menyala” akan menghentikan program lewat std::terminate. Chapter ini membahas semuanya lewat satu program teruji yang keluarannya dijaga tetap deterministik dengan disiplin “kumpulkan dulu, cetak setelah join”.
Membuat Thread dari Fungsi dan Lambda #
Konstruktor std::thread menerima sesuatu yang bisa dipanggil — function biasa, lambda, function object — plus argumen untuknya. Thread tidak menunggu: segera setelah objeknya dibuat, tugasnya mulai dieksekusi secara paralel dengan kode yang membuatnya. Kita bebas mencampur gaya: satu thread dari fungsi biasa, satu dari lambda, satu lagi dari lambda ber-capture, semuanya dalam satu program.
#include <iostream>
#include <string>
#include <thread>
// Fungsi yang dipakai sebagai tugas thread; hasil ditulis ke wadah
// milik pemanggil, bukan langsung ke cout (agar output tak berseling)
void tulisKe(const char* siapa, std::string& wadah) {
wadah = std::string("Halo dari ") + siapa;
}
// Fungsi dengan parameter referensi: butuh std::ref agar thread
// tidak membuat salinan
void tambahLima(int& x) {
x += 5;
}
int main() {
int nilai = 10;
int lain = 100;
std::string pesanA, pesanB, pesanC;
// 1) thread dari fungsi biasa; referensi eksplisit via std::ref
std::thread t1(tulisKe, "fungsi-biasa", std::ref(pesanA));
// 2) thread dari lambda tanpa capture
std::thread t2([&pesanB] { tulisKe("lambda", pesanB); });
// 3) capture by value: thread memegang salinan `nilai` (tetap 10)
std::thread t3([n = nilai, &pesanC] {
pesanC = "salinan bernilai " + std::to_string(n + 5);
});
// 4) capture by reference: lambda mengubah variabel asli
std::thread t4([&nilai] { nilai += 5; });
// 5) parameter referensi dari fungsi: bungkus dengan std::ref
std::thread t5(tambahLima, std::ref(lain));
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
// Semua pencetakan SETELAH join -> urutan output pasti
std::cout << pesanA << "\n";
std::cout << pesanB << "\n";
std::cout << pesanC << "\n";
std::cout << "nilai : " << nilai << "\n"; // diubah t4
std::cout << "lain : " << lain << "\n"; // diubah t5
return 0;
}Output yang Dihasilkan:
Halo dari fungsi-biasa
Halo dari lambda
salinan bernilai 15
nilai : 15
lain : 105Keluaran di atas sama persis di setiap eksekusi — dan itu hasil disiplin yang disengaja, bukan kebetulan. Bila kelima thread langsung mencetak ke std::cout, urutan barisnya akan bergantung pada penjadwalan: eksekusi pertama bisa membuka dengan Halo dari lambda, eksekusi kedua dengan Halo dari fungsi-biasa, bahkan dua potongan teks dari thread berbeda bisa terseling di tengah baris karena tiap panggilan << adalah operasi terpisah. Karena itu program ini memakai pola yang akan terus kita pakai sepanjang section: setiap pekerja menulis hasilnya ke wadah miliknya sendiri, dan pencetakan baru dilakukan main() setelah semua join().
Bedah barisnya:
- Baris 23 (
std::thread t1(tulisKe, "fungsi-biasa", std::ref(pesanA))): tugas berupa function biasa plus dua argumen. Argumen disalin ke dalam thread oleh konstruktor — kecuali yang dibungkusstd::ref, yang diteruskan sebagai referensi sehinggapesanAyang asli terisi. - Baris 26 (t2, lambda tanpa capture): lambda tanpa capture berlaku seperti fungsi biasa; ia menulis ke
pesanBlewat referensi yang di-capture. - Baris 29-31 (t3, capture by value):
n = nilaimenyalinnilaiSAAT lambda dibuat, bukan saat lambda dijalankan. Karena salinannya 10, baris ini selalu menghasilkan teks “salinan bernilai 15” — tidak terpengaruh oleht4yang mengubah variabel asli. - Baris 34 (
t4, capture by reference):[&nilai]membuat thread menyentuh variabel asli; setelahjoin(),nilaipasti sudah 15. Referensi ini aman karenanilaitetap hidup sampai setelahjoin(). - Baris 37 (
t5denganstd::ref(lain)): konstruktorstd::threadSELALU menyalin argumen yang dikirim ke fungsi — bahkan bila parameternya bertipeint&. Tanpastd::ref, kode ini bahkan tidak bisa dikompilasi karena thread mencoba menyalin untuk parameter referensi non-const.std::ref(lain)membungkus referensi sehinggatambahLimabekerja padalainyang asli. - Baris 39-43 (
join()):main()menunggu kelima thread.join()memblokir sampai thread-nya benar-benar berakhir, dan setelah itu objek thread menjadi “tidak menyala” (not joinable).
detach(): Kenapa Kita Menghindarinya #
Selain join(), ada detach(): thread dilepas dan berjalan sendiri tanpa bisa digabung kembali. Setelah di-detach, joinable() menjadi false dan program tidak menunggu thread itu selesai — bahkan bila main() sudah selesai lebih dulu. Masalahnya nyata: thread yang dilepas sering masih menyentuh data yang hidupnya bersama scope pembuatnya, dan kita tidak punya cara untuk mengetahui kapan ia benar-benar selesai. Menghentikannya pun tidak ada mekanisme standar yang aman.
Karena itu konvensi C++ modern berbunyi: hindari detach(). Kelola umur thread secara eksplisit dengan join(), dan bila kita butuh “kerjakan lalu abaikan hasilnya”, lebih baik pakai std::jthread dengan sinyal penghentian (chapter jthread dan stop_token) atau thread pool yang menjadi bagian desain program, bukan thread liar tanpa pemilik.
Jika Lupa join(): std::terminate #
Aturan paling keras dari std::thread adalah ini: destructor thread yang masih joinable memanggil std::terminate() — program dihentikan seketika tanpa jalur pemulihan. Kompiler tidak menolaknya, runtime tidak memberi peringatan; program yang kelihatannya normal bisa mati tiba-tiba hanya karena satu return yang melewati objek thread tanpa join().
void tugas();
int main() {
std::thread t(tugas);
// ... t keluar dari main() tanpa join() atau detach()
// destructor melihat t.joinable() == true
// -> std::terminate() dipanggil -> program berhenti
}Blok di atas sengaja tidak kita jalankan dan tidak kita klaim keluarannya. Pola yang benar sudah terlihat pada program utama chapter ini: setiap std::thread yang dibuat menerima join()-nya sebelum scope berakhir. Bila pola itu terasa melelahkan, kabar baiknya C++20 menyediakan std::jthread yang melakukan join otomatis di destructor — kita pelajari setelah melewati dasar-dasar siklus hidup thread di chapter berikutnya.
Referensi: cppreference — std::thread , cppreference — std::thread::join , cppreference — std::thread::detach .