Topik ini membahas dasar-dasar std::thread, yaitu kelas pustaka standar untuk meluncurkan thread dan mengatur umurnya. Pembahasan mencakup cara meluncurkan thread, menunggu penyelesaiannya dengan join, menyerahkannya dengan detach, serta batas-batas yang menyertai keduanya. Contoh yang dipakai membagi penghitungan satu batch nilai ujian ke dua thread, kemudian merangkum hasilnya di thread utama setelah keduanya selesai.
Meluncurkan thread pertama#
Sebuah thread adalah alur eksekusi yang berjalan sejajar dengan alur utama program di dalam proses yang sama. Thread diluncurkan dengan membuat objek std::thread yang diberi fungsi beserta argumennya. Sistem operasi menjalankan fungsi tersebut secara bersamaan dengan kode yang meluncurkannya, termasuk baris-baris setelah konstruksi objeknya. Program berikut membagi satu vektor nilai ujian menjadi dua batch, memberi setiap batch satu thread, lalu merangkum hasilnya setelah kedua thread selesai.
#include <functional>
#include <iostream>
#include <thread>
#include <vector>
void jumlahkan(const std::vector<int>& nilai, int awal, int akhir, int& hasil) {
for (int i = awal; i < akhir; ++i) {
hasil += nilai[i];
}
}
int main() {
std::vector<int> nilai = {80, 75, 90, 65, 70, 95, 85, 62};
int jumlahA = 0;
int jumlahB = 0;
std::thread tA(jumlahkan, std::cref(nilai), 0, 4, std::ref(jumlahA));
std::thread tB(jumlahkan, std::cref(nilai), 4, 8, std::ref(jumlahB));
tA.join();
tB.join();
std::cout << "jumlah batch A = " << jumlahA << "\n";
std::cout << "jumlah batch B = " << jumlahB << "\n";
std::cout << "total = " << (jumlahA + jumlahB) << "\n";
return 0;
}jumlah batch A = 310
jumlah batch B = 312
total = 622Program di atas memuat dua thread, tA dan tB, yang masing-masing menghitung jumlah satu bagian data. Fungsi jumlahkan menerima data melalui referensi konstan, sedangkan hasilnya dikembalikan melalui parameter referensi hasil. Pemanggilan tA.join() membuat thread utama berhenti sementara sampai tA selesai, dan hal yang sama berlaku untuk tB. Karena kedua pemanggilan join ditempatkan sebelum pencetakan, urutan keluaran selalu sama walaupun urutan penyelesaian kedua thread tidak dapat dijamin.
Kompilasi dan eksekusi programnya dilakukan dengan perintah berikut.
# kompilasi program dengan dukungan thread
g++ -std=c++17 -pthread jumlah-dua-thread.cpp -o /tmp/jumlah-dua-thread
# jalankan program hasil kompilasi
/tmp/jumlah-dua-threadjumlah batch A = 310
jumlah batch B = 312
total = 622Argumen thread disalin secara default#
Konstruktor std::thread menyalin atau memindahkan setiap argumen ke penyimpanan milik thread baru. Salinan ini diperlukan karena variabel lokal fungsi peluncur bisa sudah musnah ketika thread baru membacanya. Parameter yang dideklarasikan sebagai referensi harus dibungkus std::ref atau std::cref agar referensi aslinya yang diteruskan, bukan salinannya. Pada program sebelumnya, std::cref(nilai) meneruskan referensi konstan ke vektor nilai, sedangkan std::ref(jumlahA) meneruskan referensi yang boleh diubah.
Apabila pembungkus referensi itu dilupakan, argumen tetap disalin. Salinan tersebut tidak dapat diikat ke parameter referensi yang tidak konstan, sehingga compiler menolak kode itu dengan pesan error. Sebaliknya, parameter berupa referensi konstan masih menerima salinan tersebut tanpa pesan apa pun, sehingga thread bekerja pada salinan alih-alih pada data aslinya tanpa peringatan. Kebiasaan yang aman adalah menyebut std::ref atau std::cref secara eksplisit setiap kali thread diberi akses ke data milik pihak lain.
Menunggu thread dengan join#
Method join() menghentikan eksekusi thread pemanggil sampai thread yang diwakili objek tersebut selesai. Setelah join kembali, seluruh pekerjaan thread tersebut sudah berakhir dan sumber dayanya sudah dilepaskan, sehingga hasil kerjanya aman dibaca. Objek std::thread yang masih mewakili thread yang berjalan, atau disebut joinable, wajib diberi join() atau detach() sebelum objeknya dimusnahkan. Apabila destruktor dijalankan saat statusnya masih joinable, pustaka standar memanggil std::terminate dan program berakhir seketika. Peluncuran dan penyelesaian setiap thread karenanya perlu dipasangkan secara eksplisit dalam satu fungsi yang sama.
Method joinable() melaporkan apakah sebuah objek masih mewakili thread yang belum selesai. Objek yang sudah diberi join, sudah diberi detach, atau belum pernah diluncurkan melaporkan false, sehingga nilainya dapat dipakai untuk memeriksa keamanan pemanggilan berikutnya. Pemeriksaan semacam ini berguna pada fungsi yang jalannya bercabang, misalnya ketika terjadi error sebelum join sempat dipanggil.
Menyerahkan thread dengan detach#
Method detach() memutus hubungan antara objek std::thread dan thread yang diwakilinya. Setelah pemanggilan itu, thread terus berjalan sendiri di latar belakang, sedangkan objeknya segera menjadi tidak joinable dan dapat dimusnahkan dengan aman. Potongan berikut mengilustrasikan pola tersebut.
// tidak-dijalankan (proses dapat berakhir sebelum thread selesai, sehingga keluarannya tidak terjamin)
#include <iostream>
#include <thread>
void catat_log() {
for (int i = 0; i < 5; ++i) {
std::cout << "menulis log " << i << "\n";
}
}
int main() {
std::thread pekerja(catat_log);
pekerja.detach();
std::cout << "main selesai tanpa menunggu\n";
return 0;
}Potongan di atas dapat dikompilasi, tetapi keluarannya tidak dapat dipastikan. Proses main dapat berakhir sebelum thread pekerja sempat mencetak satu baris pun, sehingga sebagian atau seluruh pesannya hilang tanpa error apa pun. Risiko yang lebih serius muncul bila thread yang dilepaskan masih merujuk data milik fungsi peluncurnya, karena data itu musnah bersama fungsinya sementara thread masih membacanya. Karena tidak ada cara standar untuk menunggu thread yang sudah dilepaskan, pola detach hanya layak dipakai untuk pekerjaan yang benar-benar mandiri, misalnya pemantauan berkas konfigurasi yang umurnya sama dengan program. Untuk kebutuhan lain, join tetap menjadi pilihan yang dapat dipertanggungjawabkan.
Batas-batas std::thread#
Kelas std::thread menyelesaikan tugasnya pada tingkat paling dasar, yaitu meluncurkan eksekusi dan mengatur umurnya, tanpa mekanisme pertukaran hasil. Fungsi yang dijalankan sebuah thread bertipe void, sehingga nilai kembalinya tidak dapat diambil. Perpindahan hasil memerlukan data bersama yang dilindungi mutex, yaitu objek kunci yang hanya boleh dipegang satu thread pada satu waktu, atau alternatif berbasis tugas seperti std::async dan std::future. Kelas ini juga tidak dapat disalin, tetapi dapat dipindahkan dengan std::move, sehingga kepemilikan thread dapat diteruskan antar fungsi atau dikumpulkan dalam std::vector.
Jumlah thread yang diluncurkan sebaiknya disesuaikan dengan mesin yang menjalankannya. Method statis std::thread::hardware_concurrency() melaporkan perkiraan jumlah thread yang dapat berjalan bersamaan secara efektif pada mesin tersebut.
// tidak-dijalankan (keluarannya bergantung pada mesin yang menjalankannya)
#include <iostream>
#include <thread>
int main() {
unsigned int inti = std::thread::hardware_concurrency();
std::cout << "thread efektif: " << inti << "\n";
return 0;
}Error yang dilempar di dalam thread juga tidak dapat diteruskan ke thread lain. Apabila fungsi thread melempar exception tanpa menangkapnya sendiri, fungsi std::terminate dipanggil dan seluruh proses berakhir. Setiap fungsi yang dijalankan thread karenanya perlu menangkap exception miliknya dan menerjemahkannya menjadi nilai yang disimpan pada data bersama, misalnya kode error, agar pemanggil dapat menanganinya lebih lanjut.
Kesalahan umum#
- Meluncurkan thread lalu lupa
join. Destruktor menemukan status joinable dan memanggilstd::terminate. Perbaikannya adalah memasangkan setiap peluncuran denganjoin()pada jalur eksekusi yang sama, termasuk jalur yang melewati error. - Memanggil
join()dua kali pada objek yang sama. Pemanggilan kedua melemparstd::system_errorkarena statusnya sudah tidak joinable. Periksajoinable()terlebih dahulu bila jalur eksekusinya bercabang. - Meneruskan referensi tanpa
std::ref. Thread menerima salinan datanya, sehingga perubahan yang dihitung thread tidak pernah terlihat oleh pemanggil. - Mengharapkan thread mengembalikan nilai. Fungsi thread bertipe
void, sehingga hasilnya harus disalurkan melalui data bersama yang terlindungi, atau melalui mekanisme tugas sepertistd::async.
Ringkasan#
std::threadmeluncurkan fungsi secara bersamaan, dan argumennya disalin secara default kecuali dibungkusstd::refataustd::cref.join()menunggu penyelesaian thread, sedangkandetach()melepaskannya, dan salah satu dari keduanya wajib dilakukan sebelum objek thread dimusnahkan agar program tidak berakhir melaluistd::terminate.detachmenghilangkan kemampuan menunggu threadnya, sehingga hanya cocok untuk pekerjaan yang umurnya sama dengan program.std::threadtidak menyediakan jalur nilai kembali dan tidak dapat disalin, tetapi dapat dipindahkan denganstd::move, dan jumlah thread efektif dapat ditanyakan melaluihardware_concurrency().
Referensi#
- std::thread — cppreference — Dokumentasi resmi kelas thread beserta konstruktornya dan aturan status joinable.
- std::thread::join — cppreference — Syarat dan perilaku pemanggilan join beserta error yang mungkin dilempar.
- std::thread::detach — cppreference — Penjelasan pelepasan thread dari objeknya dan konsekuensinya.
- std::ref, std::cref — cppreference — Dokumentasi pembungkus referensi untuk argumen thread yang tidak boleh disalin.
- std::thread::hardware_concurrency — cppreference — Perilaku method penghitung thread, termasuk nilai kembalian nol.