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.

cpp Sketsa Qt — pola QThread dengan worker object (ILUSTRATIF: Qt tidak terinstal di sandbox praktikum, bentuk API untuk proyek nyata)
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.

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

text
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 responsif

Mari kita bedah baris demi baris:

  1. Baris 10-14 (KejadianProgres): satu kejadian progres membawa nomor urut, nama tugas, dan persentase — representasi argumen sinyal progres(berkas, persen) pada sketsa Qt.
  2. Baris 21-24 (dorong): std::lock_guard mengunci mutex selama push — RAII pada bagian kritis, persis pola std::lock_guard dari Section Concurrency dan Parallelism.
  3. Baris 27-35 (ambilSemua): seluruh isian antrean diambil dalam satu kali kunci, lalu mutex dilepas; thread UI bekerja pada salinannya tanpa menahan kunci.
  4. 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 ada std::cout — thread worker benar-benar beku terhadap antarmuka.
  5. Baris 65 (penyalinan antrean): AntreanProgres punya mutex, sehingga tidak bisa disalin diam-diam oleh std::thread; antrean dan daftar berkas dilewatkan secara eksplisit lewat std::ref dan std::cref.
  6. 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.
  7. 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