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::thread sudah 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, atau sleep_for yang belum habis.
  • Selesai (belum di-join): tugasnya berakhir, tetapi objek std::thread-nya masih ada dan masih joinable. Pada state inilah pemanggilan join() mengembalikan seketika dan “membersihkan” thread itu.
  • Di-join/dilepas: join() sudah dipanggil (atau detach()). Objek thread tidak lagi merepresentasikan alur eksekusi; joinable() menjadi false.

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.

cpp
#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:

text
hardware_concurrency : 2
joinable saat berjalan   : true
joinable setelah join    : false
id pekerja != id utama   : true
id default != id pekerja : true
joinable thread kosong   : false

Bedah barisnya:

  1. 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.
  2. Baris 13-16 (lambda pekerja): thread mencatat ID-nya sendiri lalu masuk state Blocked 50 milidetik lewat sleep_for. Selama itu tugasnya belum selesai.
  3. Baris 19 (joinable saat berjalan): true — objek masih merepresentasikan alur eksekusi yang hidup, entah sedang berjalan atau menunggu.
  4. Baris 21 (join()): main() menunggu sampai pekerja selesai; setelahnya transisi ke state di-join terjadi.
  5. Baris 23-24: joinable() kini false, dan ID pekerja (ditulis thread di baris 14, dibaca main() hanya setelah join() — sehingga bebas data race) berbeda dari ID main().
  6. Baris 28-29 (std::thread kosong): thread default-constructed tidak membawa tugas apa pun, jadi ia tidak pernah joinable. Itulah kenapa join()-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:

cpp cetak-id.cpp — keluarannya berubah di setiap eksekusi
#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):

text
id main   : 140214053009280
id pekerja: 140214053004992

Angka 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 .