Pengguna aplikasi desktop punya ekspektasi yang sederhana untuk diucapkan tetapi sulit untuk dipenuhi: klik apa pun harus terasa langsung, bahkan saat program sedang membaca berkas ratusan megabyte. Kemampuan itu bukan keistimewaan otomatis dari framework — ia lahir dari pembagian kerja thread yang benar. Chapter ini menjelaskan mengapa semua widget hidup di satu thread, di mana kerja berat harus dieksekusi, dan bagaimana hasilnya kembali ke antarmuka; program teruji kita membangun jembatan dua thread itu dengan pustaka standar murni, tanpa Qt dan tanpa tidur.
Satu Thread yang Memproses Semua Kejadian #
Event loop aplikasi GUI berjalan di satu thread — pada Qt, thread yang memanggil QApplication::exec(), biasanya thread main(). Semua method widget pada dasarnya membaca dan menulis state UI yang sama: geometri, konten, fokus, dan daftar kejadian internal. Karenanya Qt menetapkan aturan keras: widget hanya boleh disentuh dari thread UI. Thread lain yang memanggil label_->setText("selesai") sedang berebut state yang sama tanpa koordinasi — data race persis seperti yang kita bedah pada chapter Data Race di Section Concurrency dan Parallelism.
Perhatikan jebakannya: masalah ini tidak selesai dengan menambahkan mutex. Mengunci setiap akses widget memang menghalau data race, tetapi tidak mengembalikan kecepatan — selama thread UI sibuk mengeksekusi kerja berat, event loop tidak memproses kejadian klik maupun permintaan repaint, dan jendela tampak beku sampai kerja itu usai. Mutex menjaga kebenaran data; responsivitas menuntut hal lain, yaitu thread UI yang hanya mengerjakan pekerjaan ringan. Dua syarat itu bersama-sama memunculkan pola berikutnya.
Kerja Berat di Thread Worker #
Solusi standar Qt memisahkan dua peran. Thread UI tetap menjalankan event loop dan menjadi satu-satunya penyentuh widget. Thread worker mengeksekusi kerja berat — parsing berkas, kompresi, permintaan jaringan — tanpa menyentuh widget sama sekali. Bentuk idiomatiknya memadukan QThread dengan worker object: objek QObject biasa yang dipindahkan ke thread lain lewat moveToThread(), sehingga slot-nya dieksekusi di thread worker.
class Worker : public QObject {
Q_OBJECT
public slots:
void proses(const QString& berkas) { // berjalan di thread worker
// parsing berkas, kompresi, permintaan jaringan: kerja berat,
// tanpa menyentuh widget sama sekali
emit progres(berkas, 100);
}
signals:
void progres(const QString& berkas, int persen);
};
// di thread UI
auto* thread = new QThread(this);
auto* worker = new Worker; // tanpa parent: akan dipindah
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, [=] { worker->proses("data.csv"); });
connect(worker, &Worker::progres, this, &JendelaUtama::perbaruiBar);
thread->start(); // thread worker mulai berjalan
Dua baris connect() membagi tugas dengan tegas: sinyal started memicu pekerjaan di thread worker, dan sinyal progres membawa hasil kembali ke jendela. Perhatikan bahwa worker dibuat tanpa parent — kepemilikannya harus bebas agar bisa dipindahkan antar thread, dan pembersihannya diatur lewat sinyal finished thread yang dihubungkan ke deleteLater().
Komunikasi Balik: Queued Signal-Slot #
Worker tidak boleh memanggil widget untuk melapor; jalan keluarnya justru mekanisme chapter lalu, signal-slot. Ketika pengirim dan penerima tinggal di thread berbeda, Qt memakai queued connection: argumen sinyal dikemas ke dalam kejadian, kejadian itu diantrekan pada event loop thread penerima, dan slot dieksekusi ketika event loop penerima mengambilnya. Pengirim kembali bekerja seketika tanpa menunggu slot selesai — itulah yang membuat thread worker tidak pernah tersangkut pada kecepatan antarmuka.
Progress bar menjadi pola klasiknya. Worker memancarkan progres(persen) setiap tahap selesai; slot UI hanya menerima angka dan memanggil bar->setValue(persen) — satu panggilan ringan, sesuai syarat thread UI. Satu-satunya data yang menyeberang antar thread adalah argumen sinyal yang sudah dikemas, sehingga kita tidak mengelola lock manual pada jalur pelaporan ini.
Program Utama: Antrean Progres Dua Thread #
Program berikut membangun jembatan yang sama dengan pustaka standar: worker thread memproduksi kejadian progres ke antrean yang dijaga mutex, dan UI thread mengonsumsinya. Untuk menjaga output deterministik, UI thread menunggu worker selesai (join()) sebelum menguras antrean — pada Qt nyata, pengurasan terjadi langsung lewat queued connection sambil antarmuka tetap hidup, tetapi mekanisme antreannya sama: satu produsen, satu konsumen, data hanya lewat antrean terkunci.
#include <iostream>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <utility> // std::move, std::ref, std::cref
#include <vector>
// Satu kejadian progres: pesan yang dikirim worker menuju UI thread.
struct KejadianProgres {
int nomorUrut;
std::string namaTugas;
int persen;
};
// Antrean terkunci: satu-satunya jembatan data antara worker dan UI.
// Mutex menjamin push dari worker dan ambil dari UI tidak pernah
// terjadi bersamaan pada antrean yang sama.
class AntreanProgres {
public:
void dorong(KejadianProgres kejadian) {
std::lock_guard<std::mutex> kunci(mutex_);
antrean_.push(std::move(kejadian));
}
// Mengambil seluruh isian antrean dalam satu kali kunci.
std::vector<KejadianProgres> ambilSemua() {
std::lock_guard<std::mutex> kunci(mutex_);
std::vector<KejadianProgres> hasil;
while (!antrean_.empty()) {
hasil.push_back(std::move(antrean_.front()));
antrean_.pop();
}
return hasil;
}
private:
std::mutex mutex_;
std::queue<KejadianProgres> antrean_;
};
// Worker thread: kerja berat, TIDAK menyentuh widget sama sekali.
void prosesBerkas(AntreanProgres& antrean,
const std::vector<std::string>& daftarBerkas) {
const int total = static_cast<int>(daftarBerkas.size());
for (int i = 0; i < total; ++i) {
// Simulasi kerja berat: parsing berkas, tanpa tidur dan tanpa
// jam nyata agar output deterministik.
const int persen = (i + 1) * 100 / total;
antrean.dorong({i + 1, daftarBerkas[static_cast<std::size_t>(i)], persen});
}
}
int main() {
AntreanProgres antrean;
const std::vector<std::string> daftarBerkas = {
"penjualan-januari.csv",
"penjualan-februari.csv",
"penjualan-maret.csv",
"penjualan-april.csv",
};
// Worker dimulai sebagai thread terpisah; antrean dan daftar berkas
// dilewatkan by-reference eksplisit lewat std::ref dan std::cref.
std::thread worker(prosesBerkas, std::ref(antrean), std::cref(daftarBerkas));
worker.join(); // UI thread menunggu seluruh kerja berat selesai
// Baru setelah join UI thread mengonsumsi antrean: urutan output pasti.
std::cout << "UI thread memproses antrean kejadian:\n";
for (const KejadianProgres& kejadian : antrean.ambilSemua()) {
std::cout << " [progress] #" << kejadian.nomorUrut << " "
<< kejadian.namaTugas << " - " << kejadian.persen << "%\n";
}
std::cout << "UI thread: progress bar mencapai 100%, antarmuka tetap responsif\n";
return 0;
}Output yang Dihasilkan:
UI thread memproses antrean kejadian:
[progress] #1 penjualan-januari.csv - 25%
[progress] #2 penjualan-februari.csv - 50%
[progress] #3 penjualan-maret.csv - 75%
[progress] #4 penjualan-april.csv - 100%
UI thread: progress bar mencapai 100%, antarmuka tetap responsifMari kita bedah baris demi baris:
- Baris 10-14 (
KejadianProgres): satu kejadian progres membawa nomor urut, nama tugas, dan persentase — representasi argumen sinyalprogres(berkas, persen)pada sketsa Qt. - Baris 21-24 (
dorong):std::lock_guardmengunci mutex selama push — RAII pada bagian kritis, persis polastd::lock_guarddari Section Concurrency dan Parallelism. - Baris 27-35 (
ambilSemua): seluruh isian antrean diambil dalam satu kali kunci, lalu mutex dilepas; thread UI bekerja pada salinannya tanpa menahan kunci. - Baris 43-52 (
prosesBerkas): worker berputar atas daftar berkas dan mendorong satu kejadian per berkas. Perhatikan apa yang tidak ada di sini: tidak ada pemanggilan widget, tidak adastd::cout— thread worker benar-benar beku terhadap antarmuka. - Baris 65 (penyalinan antrean):
AntreanProgrespunya mutex, sehingga tidak bisa disalin diam-diam olehstd::thread; antrean dan daftar berkas dilewatkan secara eksplisit lewatstd::refdanstd::cref. - Baris 66 (join): UI thread menunggu worker selesai sebelum menguras antrean. Langkah inilah yang membuat urutan output pasti — semua kejadian sudah masuk sebelum satu pun dicetak.
- Baris 69-73 (konsumsi): UI thread menguras antrean dan mencetak setiap kejadian; pada aplikasi nyata, pencetakan inilah yang digantikan
bar->setValue(persen)di dalam slot.
Urutan output membuktikan kontraknya: nomor urut 1 sampai 4 tercetak rapi, dan persentase naik 25% per tahap karena worker mendorong kejadian secara berurutan ke antrean FIFO. Konsep yang dipakai di sini — std::thread, std::mutex, std::lock_guard, dan bahaya data race — seluruhnya dibahas tuntas pada Section Concurrency dan Parallelism; chapter ini hanya memakainya untuk tujuan baru, yaitu menjaga antarmuka tetap hidup.
Dengan Qt, kita memiliki pohon widget yang bertahan dan sistem lengkap di sekelilingnya — moc, layout, thread. Untuk sebagian keperluan, terutama tooling internal, pohon widget itu justru beban yang tidak perlu; chapter penutup section ini mengenalkan Dear ImGui yang membalik model mentalnya.
Referensi: doc.qt.io — QThread , en.cppreference.com — std::thread