Sejauh ini thread yang kita buat menyimpan hasilnya ke variabel bersama — efektif, tetapi setiap variabel bersama membawa kewajiban sinkronisasi. Pustaka standar menawarkan jalur yang lebih elegan: std::promise dan std::future, sepasang objek yang membentuk kanal satu arah. Thread penghasil memasukkan nilainya lewat promise; thread penunggu mengambilnya lewat future — dan get() otomatis menunggu sampai nilai benar-benar tiba. Bonus yang sering menentukan: exception yang dilempar di thread pekerja juga bisa dibawa lewat kanal yang sama, sesuatu yang tidak mungkin dengan variabel biasa. Chapter ini menguji ketiganya: nilai, exception, dan shared_future untuk banyak pembaca.
promise dan future: Kanal Satu Arah #
Polanya selalu sama. Di sisi pengirim dibuat std::promise<T>; di sisi penerima diambil std::future<T> lewat get_future(). Keduanya harus terhubung sebelum thread dibuat, karena objek-objek itu tidak bisa disalin — yang dikirim ke thread adalah referensinya. set_value() dipanggil sekali saja; get() boleh dipanggil sekali saja, dan bila nilai belum ada, get() menunggu (state Blocked) sampai tiba.
Nilai, Exception, dan Banyak Pembaca #
Program teruji berikut menjalankan tiga skenario dalam satu file: mengirim hasil hitungan, mengirim exception, dan menyebarkan satu hasil ke dua pembaca lewat shared_future.
#include <future>
#include <iostream>
#include <stdexcept>
#include <thread>
#include <vector>
int jumlahKuadrat(int n) {
int total = 0;
for (int i = 1; i <= n; ++i) {
total += i * i;
}
return total;
}
int main() {
// 1) promise -> future: kanal satu arah untuk satu nilai
std::promise<int> janji;
std::future<int> hasil = janji.get_future();
std::thread pekerja([&janji] {
janji.set_value(jumlahKuadrat(10)); // kirim hasil lewat kanal
});
std::cout << "jumlah kuadrat : " << hasil.get() << "\n";
pekerja.join();
// 2) exception juga bisa dikirim lewat kanal yang sama
std::promise<int> janjiGagal;
std::future<int> hasilGagal = janjiGagal.get_future();
std::thread pekerjaGagal([&janjiGagal] {
try {
throw std::runtime_error("pembagi nol");
} catch (...) {
janjiGagal.set_exception(std::current_exception());
}
});
try {
hasilGagal.get();
} catch (const std::exception& e) {
std::cout << "exception lewat future: " << e.what() << "\n";
}
pekerjaGagal.join();
// 3) shared_future: satu hasil, boleh dibaca banyak thread
std::promise<int> janjiBersama;
std::shared_future<int> bersama = janjiBersama.get_future().share();
janjiBersama.set_value(42);
std::vector<int> tangkapan(2);
std::vector<std::thread> pembaca;
for (int i = 0; i < 2; ++i) {
pembaca.emplace_back([&bersama, &tangkapan, i] {
tangkapan[i] = bersama.get(); // get() boleh dipanggil berulang
});
}
for (std::thread& t : pembaca) {
t.join();
}
std::cout << "shared_future : " << tangkapan[0] << ", "
<< tangkapan[1] << "\n";
return 0;
}Output yang Dihasilkan:
jumlah kuadrat : 385
exception lewat future: pembagi nol
shared_future : 42, 42Seluruh keluaran deterministik karena setiap get() berpasangan dengan satu set_value yang sudah pasti, dan pencetakan berada di main() setelah join(). Bedah skenarionya:
- Baris 19-20 (kanal dibuat):
promisemilik thread pekerja,futuremilikmain(). Satu promise boleh mengirim ke satu future — pasangan satu-ke-satu. - Baris 22 (
set_value(...)): pekerja selesai dan mengirim 1²+2²+…+10² = 385. Setelah panggilan ini,get()di sisi lain langsung melanjutkan. - Baris 24 (
hasil.get()): bila pekerja belum selesai, baris ini menunggu; bila sudah, nilai langsung terambil. Perhatikan urutan yang aman:get()dulu, barujoin()— keduanya menutup kanal dengan rapi. - Baris 30-35 (pengiriman exception): exception ditangkap lewat
catch (...), lalu dikirim apa adanya lewatset_exception(std::current_exception()). Yang berpindah bukan pesannya saja, tapi objek exception utuh. - Baris 39-44 (penerimaan):
get()melempar ulang exception di sisi penerima — threadmain()menangkapnya seolah exception itu dilempar di tempatnya sendiri. Tanpa mekanisme ini, exception di thread pekerja akan terkubur di sana. - Baris 49-50 (
share()):futurebiasa hanya bisa di-get()sekali;shared_futurebisa dibaca banyak thread berulang-ulang. Nilai 42 di-set darimain()sebelum thread pembaca dibuat, sehingga kedua pembaca pasti mendapatkan 42.
packaged_task: Membungkus Callable untuk Future #
std::promise bagus saat kita mengirim hasil satu per satu secara manual, tetapi pola yang lebih umum adalah “jalankan fungsi ini di thread lain, kembalikan lewat future”. Menulis promise dan memanggilnya satu per satu terasa berulang. Untuk itu C++11 menyediakan std::packaged_task: pembungkus callable yang memasangkan dirinya dengan future secara otomatis — saat callable dijalankan, nilai kembaliannya (atau exception-nya) mengalir ke future yang diambil lewat get_future().
#include <future>
#include <iostream>
int main() {
// packaged_task membungkus fungsi agar hasilnya bisa ditarik lewat future.
std::packaged_task<int(int, int)> tugas{[](int a, int b) {
return a * b;
}};
std::future<int> hasil = tugas.get_future();
std::thread pelaksana(std::move(tugas), 6, 7);
std::cout << "nilai future = " << hasil.get() << "\n";
pelaksana.join();
return 0;
}Output yang Dihasilkan:
nilai future = 42Mari kita telaah alurnya:
- Baris 6-8 (
packaged_task): tipe template-nya menyebut tanda tangan lengkapint(int, int)— callable apa pun yang cocok bisa ditampung, di sini lambda pengali. - Baris 9 (
get_future()): future diambil sebelum tugas dieksekusi, persis seperti pada promise. - Baris 10 (
std::move(tugas)):packaged_taskhanya-bisa-dipindah (move-only) karena hanya boleh dijalankan sekali; menjalankannya di thread pelaksana sekaligus mengoperasikan argumen6, 7. - Baris 11 (
hasil.get()): thread utama menunggu sampai perhitungan selesai, lalu menerima42— tanpa satu pun penulisan mutex manual.
Dalam skema course ini, tiga alat saling melengkapi: std::async paling singkat untuk sekali-jalan, std::promise untuk kendali penuh atas nilai yang dikirim, dan std::packaged_task untuk membungkus callable yang jadwalnya ditentukan pemiliknya — misalnya antrean tugas thread pool pada chapter berikutnya.
shared_future: Satu Hasil, Banyak Pembaca #
Kapan shared_future berguna? Bila satu hasil kerja dibutuhkan beberapa pihak — misalnya satu kali muat konfigurasi yang dibaca banyak worker, atau satu hasil validasi yang ditunggu beberapa pengirim lanjutan. Mekanisme menunggunya sama persis: tiap get() menunggu sampai nilai siap, dan nilai yang sudah siap dibaca tanpa saling mengganggu.
Kanal promise-future menuntaskan pengiriman hasil yang sudah ada di tangan. Pertanyaan berikutnya lebih praktis: bagaimana bila kita hanya ingin “kerjakan ini di thread lain, kembalikan hasilnya”? Untuk kebutuhan itu standar menyediakan pembungkus yang jauh lebih ringkas: std::async.
Referensi: cppreference — std::promise , cppreference — std::future , cppreference — std::shared_future .