Setiap std::thread yang kita buat melewati rangkaian state — dari baru diciptakan, berjalan, kadang menunggu, sampai selesai — dan memahami rangkaian itu menjelaskan banyak perilaku yang tampak aneh pada pemula. Kenapa join() bisa dipanggil dua kali lalu program berhenti? Kenapa thread tanpa tugas tidak bisa di-join? Kenapa keluaran dua thread tidak pernah rapi berurutan? Semua jawabannya ada pada siklus hidup ini. Chapter ini memetakan state-nya, lalu mengamati joinable, jumlah core, dan identitas thread lewat dua program teruji.
State dalam Siklus Hidup Thread #
Standar C++ tidak mendaftar state seketat bahasa lain, tetapi secara praktis sebuah thread melewati lima keadaan berikut:
- Baru: objek
std::threadsudah dibuat; tugasnya baru saja dikirim ke scheduler dan bisa mulai kapan saja. - Runnable/berjalan: tugas benar-benar dieksekusi, atau siap dieksekusi dan hanya menunggu giliran CPU dari scheduler.
- Blocked: thread terhenti sementara karena menunggu sesuatu — kunci mutex, kondisi di
condition_variable, atausleep_foryang belum habis. - Selesai (belum di-join): tugasnya berakhir, tetapi objek
std::thread-nya masih ada dan masih joinable. Pada state inilah pemanggilanjoin()mengembalikan seketika dan “membersihkan” thread itu. - Di-join/dilepas:
join()sudah dipanggil (ataudetach()). Objek thread tidak lagi merepresentasikan alur eksekusi;joinable()menjadifalse.
Diagram di atas merangkum alurnya: state Blocked dan Berjalan boleh bergantian berkali-kali sebelum thread tiba di Selesai. Yang penting diingat, transisi ke Selesai terjadi sendirinya saat tugas berakhir — join() tidak menghentikan apa pun, ia hanya menunggu dan menutup.
Mengamati joinable dan ID lewat Program #
Program berikut memeriksa joinable() di beberapa titik siklus hidup, membandingkan ID thread, dan membaca hardware_concurrency() — jumlah thread yang bisa dijalankan CPU secara bersamaan.
#include <chrono>
#include <iostream>
#include <thread>
using namespace std::chrono_literals;
int main() {
std::cout << "hardware_concurrency : "
<< std::thread::hardware_concurrency() << "\n";
std::thread::id idUtama = std::this_thread::get_id();
std::thread::id idPekerja{};
std::thread pekerja([&idPekerja] {
idPekerja = std::this_thread::get_id();
std::this_thread::sleep_for(50ms); // simulasi state blocked
});
std::cout << std::boolalpha;
std::cout << "joinable saat berjalan : " << pekerja.joinable() << "\n";
pekerja.join(); // menunggu sampai thread benar-benar selesai
std::cout << "joinable setelah join : " << pekerja.joinable() << "\n";
std::cout << "id pekerja != id utama : " << (idPekerja != idUtama) << "\n";
std::cout << "id default != id pekerja : "
<< (std::thread::id{} != idPekerja) << "\n";
std::thread kosong; // thread tanpa tugas
std::cout << "joinable thread kosong : " << kosong.joinable() << "\n";
return 0;
}Output yang Dihasilkan:
hardware_concurrency : 2
joinable saat berjalan : true
joinable setelah join : false
id pekerja != id utama : true
id default != id pekerja : true
joinable thread kosong : falseBedah barisnya:
- Baris 7-8 (
hardware_concurrency()): mesin uji ini punya 2 core logis. Angkanya hanya petunjuk — bisa 0 bila sistem tidak memberi tahu — dan di mesin lain pasti berbeda. Nilai ini dasar yang baik untuk menentukan berapa banyak thread pekerja yang layak dibuat. - Baris 13-16 (lambda pekerja): thread mencatat ID-nya sendiri lalu masuk state Blocked 50 milidetik lewat
sleep_for. Selama itu tugasnya belum selesai. - Baris 19 (
joinable saat berjalan):true— objek masih merepresentasikan alur eksekusi yang hidup, entah sedang berjalan atau menunggu. - Baris 21 (
join()):main()menunggu sampai pekerja selesai; setelahnya transisi ke state di-join terjadi. - Baris 23-24:
joinable()kinifalse, dan ID pekerja (ditulis thread di baris 14, dibacamain()hanya setelahjoin()— sehingga bebas data race) berbeda dari IDmain(). - Baris 28-29 (
std::thread kosong): thread default-constructed tidak membawa tugas apa pun, jadi ia tidak pernah joinable. Itulah kenapajoin()-nya tidak boleh dipanggil.
ID Thread: Pembanding yang Aman, Teks yang Opaque #
Bentuk pembandingan itulah yang kita pakai pada program utama di atas: get_id() mengembalikan std::thread::id, tipe yang perbandingannya (==, !=) boleh dipakai untuk mengenali thread — contohnya agar fungsi menolak dipanggil dari thread tertentu. Tetapi bentuk teksnya tidak diatur standar: std::thread::id bersifat opaque, dan libstdc++ merendernya sebagai angka besar yang berubah antar eksekusi. Program berikut mencetak ID mentah — karena keluarannya pasti berbeda di setiap eksekusi, ia kita tampilkan sebagai contoh, bukan sebagai output terverifikasi:
#include <iostream>
#include <thread>
int main() {
std::cout << "id main : " << std::this_thread::get_id() << "\n";
std::thread pekerja([] {
std::cout << "id pekerja: " << std::this_thread::get_id() << "\n";
});
pekerja.join();
return 0;
}Contoh keluaran satu eksekusi (angkanya PASTI berbeda di eksekusi lain, bahkan di mesin yang sama, dan urutan dua barisnya juga bisa bertukar karena kedua thread mencetak hampir bersamaan):
id main : 140214053009280
id pekerja: 140214053004992Angka yang tampak adalah alamat internal, bukan nomor urut yang stabil. Pelajaran pentingnya untuk debugging: jangan pernah mengandalkan nilai ID untuk logika program — pakai perbandingan ID untuk mengenali thread, dan bila perlu mencatat ID dalam log, perlakukan teksnya sebagai penanda sesi, bukan data yang bisa dibandingkan antar eksekusi.
Dengan memahami state, joinable(), dan ID, kita punya kosakata untuk membahas masalah klasik berikutnya: dua thread menyentuh data yang sama tanpa aturan. Chapter tentang std::mutex menunjukkan apa yang terjadi dan bagaimana mencegahnya.
Referensi: cppreference — std::thread::hardware_concurrency , cppreference — std::thread::get_id , cppreference — std::thread::joinable .