Program paralel punya dua pertanyaan yang berbeda dan mudah tertukar: apakah jawabannya benar, dan apakah eksekusinya lebih cepat. Pertanyaan pertama harus dijawab dengan bukti yang identik setiap eksekusi; pertanyaan kedua memang sifatnya berubah-ubah antar mesin dan antar hari. Bagian penutup ini menuntaskan keduanya secara terpisah: kebenaran dikunci oleh harness dengan 29 cek yang membandingkan hasil paralel terhadap versi single-thread, sedangkan performa diukur lewat mode khusus --waktu yang sengaja tidak pernah masuk output terverifikasi. Dengan dua jalur ini, kita menutup proyek dengan program yang bukan hanya berjalan, tetapi terbukti benar.
Strategi Pengujian Program Paralel #
Menguji program paralel terasa sulit karena urutan eksekusinya tak terkendali. Kuncinya: jangan pernah menguji urutan — uji hasil akhirnya yang sifatnya pasti. Harness kita memakai enam kelompok uji:
| Uji | Apa yang dibuktikan |
|---|---|
buatData dua pemanggilan | PRNG berseed tetap menghasilkan data identik |
bagiRentang(10, 4) | sisa pembagian jatuh ke thread terdepan |
| cakupan seluruh rentang | tanpa celah dan tanpa tumpang tindih; total panjang = total data |
| kasus tepi 8/4 dan 0/3 | pembagian rata dan data kosong tidak merusak function |
jumlahRentang data kecil | nilai yang sudah dihitung tangan (14, 7, 0) |
| paralel 3/4/7 thread vs tunggal | kebenaran tidak bergantung pada jumlah thread |
Enam kelompok itu sengaja tidak memuat satu pun pengujian soal kapan thread berjalan. Data deterministik dari Bagian 1 membuat seluruh perbandingan ini bisa diulang tanpa batas, dan hasilnya tetap sama — itulah yang membuatnya layak disebut kontrak.
Kunci Output pada Hasil Agregat #
Prinsip yang sama berlaku untuk output program finalnya. Program hanya mencetak nilai agregat: jumlah gabungan, isi parsial berdasarkan nomor id, dan statistik yang sudah selesai digabung dengan mutex. Tidak ada satu baris pun yang menyebut thread mana yang menyelesaikan rentangnya lebih dulu, karena kronologi itu tidak menentukan jawaban. Hasil parsial tercetak urut id — urutan yang kita tetapkan, bukan urutan yang diberikan penjadwal.
Program Final Lengkap #
Program final menyatukan empat bagian: pembagian rentang, eksekusi paralel, sinkronisasi statistik, dan verifikasi internal. Kompilasi dengan -pthread — kebiasaan yang aman untuk program thread; di lingkungan uji kita dengan glibc modern, program ini juga terkompilasi tanpa flag tersebut — lalu jalankan:
g++ -std=c++23 -Wall -Wextra -pthread multithread-final.cpp -o multithread
./multithreadBerikut isi lengkap src/multithread.cpp:
// multithread.cpp — Proyek: Aplikasi Multithreaded
// Menjumlahkan 4.000.000 angka secara paralel dengan pola partial sum,
// lalu membandingkan hasilnya dengan versi single-thread. Data dibuat
// dari PRNG berseed tetap dan hasil akhir berupa integer, sehingga
// output agregat program selalu sama di setiap eksekusi.
//
// Cara pakai:
// multithread → hasil agregat (deterministik)
// multithread --waktu → tambah durasi eksekusi (nilainya bervariasi)
#include <chrono>
#include <cstdint>
#include <mutex>
#include <print>
#include <random>
#include <string>
#include <thread>
#include <vector>
constexpr std::size_t JUMLAH_DATA = 4'000'000;
constexpr int JUMLAH_THREAD = 4;
constexpr int AMBANG = 500;
// ---------- Tipe data ----------
// Satu potongan data (chunk) milik satu thread.
struct Rentang {
std::size_t awal = 0;
std::size_t panjang = 0;
};
// Hasil eksekusi paralel.
struct HasilParalel {
std::int64_t jumlah = 0; // hasil gabungan seluruh thread
std::int64_t diAtasAmbang = 0; // statistik: elemen > AMBANG
std::vector<std::int64_t> parsial; // jumlah per thread, urut id thread
};
// ---------- Data uji yang deterministik ----------
std::vector<int> buatData(std::size_t jumlah) {
std::mt19937 mesin(42); // seed tetap → deret angka selalu sama
std::uniform_int_distribution<int> sebar(-1000, 1000);
std::vector<int> data(jumlah);
for (int& nilai : data) {
nilai = sebar(mesin);
}
return data;
}
// ---------- Pembagian pekerjaan ----------
// Membagi total elemen ke beberapa rentang berurutan tanpa celah dan tanpa
// tumpang tindih. Sisa pembagian dibagikan satu per satu ke thread terdepan.
std::vector<Rentang> bagiRentang(std::size_t total, int jumlahThread) {
std::vector<Rentang> daftar;
daftar.reserve(static_cast<std::size_t>(jumlahThread));
const std::size_t dasar = total / jumlahThread;
const std::size_t sisa = total % jumlahThread;
std::size_t awal = 0;
for (int i = 0; i < jumlahThread; ++i) {
const std::size_t panjang =
dasar + (static_cast<std::size_t>(i) < sisa ? 1 : 0);
daftar.push_back({awal, panjang});
awal += panjang;
}
return daftar;
}
// ---------- Perhitungan per potongan ----------
std::int64_t jumlahRentang(const std::vector<int>& data, const Rentang& rentang) {
std::int64_t total = 0;
for (std::size_t i = rentang.awal; i < rentang.awal + rentang.panjang; ++i) {
total += data[i];
}
return total;
}
std::int64_t hitungDiAtasAmbang(const std::vector<int>& data, const Rentang& rentang) {
std::int64_t hitung = 0;
for (std::size_t i = rentang.awal; i < rentang.awal + rentang.panjang; ++i) {
if (data[i] > AMBANG) {
++hitung;
}
}
return hitung;
}
// ---------- Eksekusi paralel ----------
HasilParalel jalankanParalel(const std::vector<int>& data, int jumlahThread) {
const std::vector<Rentang> daftar = bagiRentang(data.size(), jumlahThread);
// Pola collect-then-merge: tiap thread menulis ke slot hasil miliknya
// sendiri. Slot berbeda = alamat memori berbeda = bebas data race.
std::vector<std::int64_t> parsial(static_cast<std::size_t>(jumlahThread), 0);
// Statistik gabungan berupa satu variable bersama → wajib dilindungi
// mutex agar penambahannya tidak saling menimpa.
std::int64_t diAtasAmbang = 0;
std::mutex kunciStatistik;
std::vector<std::jthread> pekerja;
pekerja.reserve(static_cast<std::size_t>(jumlahThread));
for (int id = 0; id < jumlahThread; ++id) {
pekerja.emplace_back([id, &data, &daftar, &parsial, &diAtasAmbang,
&kunciStatistik] {
const std::size_t i = static_cast<std::size_t>(id);
parsial[i] = jumlahRentang(data, daftar[i]);
const std::int64_t kontribusi = hitungDiAtasAmbang(data, daftar[i]);
const std::lock_guard<std::mutex> penjaga(kunciStatistik);
diAtasAmbang += kontribusi;
});
}
// Jthread otomatis di-join oleh destructor, tetapi kita butuh hasilnya
// SEKARANG, jadi join secara eksplisit sebelum menggabungkan.
for (std::jthread& t : pekerja) {
t.join();
}
// Fase merge: thread utama menjumlahkan seluruh hasil parsial.
HasilParalel keluar;
for (const std::int64_t p : parsial) {
keluar.jumlah += p;
}
keluar.diAtasAmbang = diAtasAmbang;
keluar.parsial = std::move(parsial);
return keluar;
}
// main() dibungkus #ifndef MULTITHREAD_UJI: program uji (uji-multithread.cpp)
// meng-include berkas ini dengan macro MULTITHREAD_UJI aktif, sehingga
// main() asli tidak ikut terkompilasi dan seluruh fungsi di atas bisa
// diuji langsung tanpa konflik.
#ifndef MULTITHREAD_UJI
int main(int argc, char* argv[]) {
const bool tampilkanWaktu = argc > 1 && std::string(argv[1]) == "--waktu";
std::println("Menyiapkan {} elemen (seed 42) ...", JUMLAH_DATA);
const std::vector<int> data = buatData(JUMLAH_DATA);
auto mulaiTunggal = std::chrono::steady_clock::now();
const std::int64_t jumlahTunggal = jumlahRentang(data, Rentang{0, data.size()});
auto selesaiTunggal = std::chrono::steady_clock::now();
auto mulaiParalel = std::chrono::steady_clock::now();
const HasilParalel hasil = jalankanParalel(data, JUMLAH_THREAD);
auto selesaiParalel = std::chrono::steady_clock::now();
std::println("single-thread : {}", jumlahTunggal);
std::println("multi-thread : {} ({} thread)", hasil.jumlah, JUMLAH_THREAD);
std::print("partial sum :");
for (const std::int64_t p : hasil.parsial) {
std::print(" {}", p);
}
std::println("");
std::println("elemen > {} : {} (statistik digabung dengan mutex)", AMBANG,
hasil.diAtasAmbang);
const bool lulus = jumlahTunggal == hasil.jumlah;
std::println("verifikasi : {}",
lulus ? "LULUS (hasil sama)" : "GAGAL (hasil berbeda)");
if (tampilkanWaktu) {
const auto msTunggal = std::chrono::duration_cast<std::chrono::milliseconds>(
selesaiTunggal - mulaiTunggal);
const auto msParalel = std::chrono::duration_cast<std::chrono::milliseconds>(
selesaiParalel - mulaiParalel);
std::println("waktu single : {} ms", msTunggal.count());
std::println("waktu multi : {} ms", msParalel.count());
}
return lulus ? 0 : 1;
}
#endifOutput yang Dihasilkan:
Menyiapkan 4000000 elemen (seed 42) ...
single-thread : -523407
multi-thread : -523407 (4 thread)
partial sum : 356127 422608 -307712 -994430
elemen > 500 : 998349 (statistik digabung dengan mutex)
verifikasi : LULUS (hasil sama)Cara membacanya dari atas ke bawah: 4.000.000 elemen disiapkan dari seed 42; versi single-thread menjumlahkannya menjadi -523407; versi 4 thread tiba di angka yang sama persis. Baris partial sum membuka bagian dalamnya — 356127, 422608, -307712, -994430 — jumlah tiap rentang yang masing-masing berisi satu juta elemen itu, dan penjumlahan keempatnya mengembalikan -523407. Statistik elemen > 500 bernilai 998349, masuk akal untuk sebaran -1000..1000, dan baris verifikasi: LULUS (hasil sama) adalah program yang memeriksa dirinya sendiri sebelum keluar dengan exit code 0. Jalankan berkali-kali: empat thread itu mungkin bekerja dalam urutan yang berbeda-beda di balik layar, tetapi semua angka di atas tidak pernah bergeser.
Harness: 29 Cek Hijau #
Harness mengompilasi kode sumber yang sama dengan macro MULTITHREAD_UJI aktif — pola #ifndef yang kita pakai pada proyek pengelola file — sehingga yang diuji adalah function aslinya, bukan salinannya:
g++ -std=c++23 -Wall -Wextra -pthread multithread-uji.cpp -o multithread-uji
./multithread-ujiIsi tests/uji-multithread.cpp, disalin ke bundle Bagian ini dengan jalur include yang menunjuk multithread-final.cpp di folder yang sama:
// multithread-uji.cpp — harness pengujian untuk multithread-final.cpp.
// Meng-include berkas sumber asli dengan macro MULTITHREAD_UJI aktif agar
// main() tidak ikut terkompilasi — yang diuji adalah fungsi aslinya,
// bukan salinannya. Berkas multithread-final.cpp harus berada di folder
// yang sama dengan berkas ini.
#define MULTITHREAD_UJI
#include "multithread-final.cpp"
static int jumlahLulus = 0;
static int jumlahGagal = 0;
static std::string namaUjiAktif;
#define MULAI_UJI(nama) namaUjiAktif = (nama)
#define CHECK_TRUE(kondisi, deskripsi) \
cekBoolean((kondisi), (deskripsi), #kondisi)
#define CHECK_EQ(aktual, diharapkan) \
cekSama((aktual), (diharapkan), #aktual)
void cekBoolean(bool kondisi, const std::string& deskripsi, const char* ekspresi) {
if (kondisi) {
++jumlahLulus;
std::println("[ LULUS ] {} - {}", namaUjiAktif, deskripsi);
} else {
++jumlahGagal;
std::println("[ GAGAL ] {} - {} ({})", namaUjiAktif, deskripsi, ekspresi);
}
}
template <typename T, typename U>
void cekSama(const T& aktual, const U& diharapkan, const char* ekspresi) {
if (aktual == diharapkan) {
++jumlahLulus;
std::println("[ LULUS ] {} - {}", namaUjiAktif, ekspresi);
} else {
++jumlahGagal;
std::println("[ GAGAL ] {} - {} (aktual {}, diharapkan {})",
namaUjiAktif, ekspresi, aktual, diharapkan);
}
}
void ujiBuatDataDeterministik() {
MULAI_UJI("buatData deterministik dengan seed tetap");
const std::vector<int> pertama = buatData(1000);
const std::vector<int> kedua = buatData(1000);
CHECK_TRUE(pertama == kedua, "dua pemanggilan menghasilkan data identik");
CHECK_EQ(pertama.size(), std::size_t(1000));
bool dalamBatas = true;
for (const int nilai : pertama) {
if (nilai < -1000 || nilai > 1000) dalamBatas = false;
}
CHECK_TRUE(dalamBatas, "seluruh nilai berada pada rentang -1000..1000");
}
void ujiBagiRentang() {
MULAI_UJI("bagiRentang 10 elemen ke 4 thread");
const std::vector<Rentang> daftar = bagiRentang(10, 4);
CHECK_EQ(daftar.size(), std::size_t(4));
CHECK_EQ(daftar[0].awal, std::size_t(0));
CHECK_EQ(daftar[0].panjang, std::size_t(3));
CHECK_EQ(daftar[1].awal, std::size_t(3));
CHECK_EQ(daftar[2].panjang, std::size_t(2));
CHECK_EQ(daftar[3].awal, std::size_t(8));
CHECK_EQ(daftar[3].panjang, std::size_t(2));
}
void ujiBagiRentangTanpaCelah() {
MULAI_UJI("bagiRentang menutup seluruh elemen tanpa tumpang tindih");
const std::vector<Rentang> daftar = bagiRentang(JUMLAH_DATA, JUMLAH_THREAD);
std::size_t cakupan = 0;
std::size_t awalDiharapkan = 0;
bool tanpaCelah = true;
for (const Rentang& r : daftar) {
if (r.awal != awalDiharapkan) tanpaCelah = false;
awalDiharapkan += r.panjang;
cakupan += r.panjang;
}
CHECK_TRUE(tanpaCelah, "awal tiap rentang berlanjut dari rentang sebelumnya");
CHECK_EQ(cakupan, JUMLAH_DATA);
}
void ujiBagiRentangKasusTepi() {
MULAI_UJI("bagiRentang kasus tepi");
const std::vector<Rentang> samaRata = bagiRentang(8, 4);
CHECK_EQ(samaRata[0].panjang, std::size_t(2));
CHECK_EQ(samaRata[3].panjang, std::size_t(2));
const std::vector<Rentang> kosong = bagiRentang(0, 3);
CHECK_EQ(kosong.size(), std::size_t(3));
CHECK_EQ(kosong[0].panjang, std::size_t(0));
}
void ujiJumlahRentang() {
MULAI_UJI("jumlahRentang pada data kecil");
const std::vector<int> data = {5, -3, 10, 2};
CHECK_EQ(jumlahRentang(data, Rentang{0, 4}), std::int64_t(14));
CHECK_EQ(jumlahRentang(data, Rentang{1, 2}), std::int64_t(7));
CHECK_EQ(jumlahRentang(data, Rentang{0, 0}), std::int64_t(0));
}
void ujiParalelSamaDenganTunggal() {
MULAI_UJI("hasil paralel identik dengan single-thread");
const std::vector<int> data = buatData(100000);
const std::int64_t tunggal = jumlahRentang(data, Rentang{0, data.size()});
const HasilParalel hasil4 = jalankanParalel(data, 4);
CHECK_EQ(hasil4.jumlah, tunggal);
CHECK_EQ(hasil4.parsial.size(), std::size_t(4));
std::int64_t gabungan = 0;
std::int64_t hitungAmbang = 0;
for (const std::int64_t p : hasil4.parsial) gabungan += p;
for (const int nilai : data) {
if (nilai > AMBANG) ++hitungAmbang;
}
CHECK_EQ(gabungan, tunggal);
CHECK_EQ(hasil4.diAtasAmbang, hitungAmbang);
}
void ujiParalelDenganPembagianLain() {
MULAI_UJI("hasil tidak bergantung pada jumlah thread");
const std::vector<int> data = buatData(100000);
const std::int64_t tunggal = jumlahRentang(data, Rentang{0, data.size()});
const HasilParalel hasil3 = jalankanParalel(data, 3);
const HasilParalel hasil7 = jalankanParalel(data, 7);
CHECK_EQ(hasil3.jumlah, tunggal);
CHECK_EQ(hasil7.jumlah, tunggal);
CHECK_EQ(hasil3.parsial.size(), std::size_t(3));
CHECK_EQ(hasil7.parsial.size(), std::size_t(7));
}
void ujiDataKosong() {
MULAI_UJI("data kosong menghasilkan jumlah nol");
const HasilParalel hasil = jalankanParalel({}, 4);
CHECK_EQ(hasil.jumlah, std::int64_t(0));
CHECK_EQ(hasil.diAtasAmbang, std::int64_t(0));
}
int main() {
ujiBuatDataDeterministik();
ujiBagiRentang();
ujiBagiRentangTanpaCelah();
ujiBagiRentangKasusTepi();
ujiJumlahRentang();
ujiParalelSamaDenganTunggal();
ujiParalelDenganPembagianLain();
ujiDataKosong();
std::println("");
std::println("ringkasan: {} lulus, {} gagal", jumlahLulus, jumlahGagal);
return jumlahGagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
[ LULUS ] buatData deterministik dengan seed tetap - dua pemanggilan menghasilkan data identik
[ LULUS ] buatData deterministik dengan seed tetap - pertama.size()
[ LULUS ] buatData deterministik dengan seed tetap - seluruh nilai berada pada rentang -1000..1000
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar.size()
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar[0].awal
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar[0].panjang
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar[1].awal
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar[2].panjang
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar[3].awal
[ LULUS ] bagiRentang 10 elemen ke 4 thread - daftar[3].panjang
[ LULUS ] bagiRentang menutup seluruh elemen tanpa tumpang tindih - awal tiap rentang berlanjut dari rentang sebelumnya
[ LULUS ] bagiRentang menutup seluruh elemen tanpa tumpang tindih - cakupan
[ LULUS ] bagiRentang kasus tepi - samaRata[0].panjang
[ LULUS ] bagiRentang kasus tepi - samaRata[3].panjang
[ LULUS ] bagiRentang kasus tepi - kosong.size()
[ LULUS ] bagiRentang kasus tepi - kosong[0].panjang
[ LULUS ] jumlahRentang pada data kecil - jumlahRentang(data, Rentang{0, 4})
[ LULUS ] jumlahRentang pada data kecil - jumlahRentang(data, Rentang{1, 2})
[ LULUS ] jumlahRentang pada data kecil - jumlahRentang(data, Rentang{0, 0})
[ LULUS ] hasil paralel identik dengan single-thread - hasil4.jumlah
[ LULUS ] hasil paralel identik dengan single-thread - hasil4.parsial.size()
[ LULUS ] hasil paralel identik dengan single-thread - gabungan
[ LULUS ] hasil paralel identik dengan single-thread - hasil4.diAtasAmbang
[ LULUS ] hasil tidak bergantung pada jumlah thread - hasil3.jumlah
[ LULUS ] hasil tidak bergantung pada jumlah thread - hasil7.jumlah
[ LULUS ] hasil tidak bergantung pada jumlah thread - hasil3.parsial.size()
[ LULUS ] hasil tidak bergantung pada jumlah thread - hasil7.parsial.size()
[ LULUS ] data kosong menghasilkan jumlah nol - hasil.jumlah
[ LULUS ] data kosong menghasilkan jumlah nol - hasil.diAtasAmbang
ringkasan: 29 lulus, 0 gagalDua uji terakhir dari daftar strategi layak dibedah karena merekalah inti bukti kebenaran. ujiParalelSamaDenganTunggal menghitung jumlah referensi dengan jumlahRentang satu-thread, lalu menuntut hasil4.jumlah == tunggal, menggabungkan sendiri keempat slot parsial dan menuntut hasil yang sama, serta menghitung ulang statistik ambang dengan loop biasa untuk membandingkan hasil yang digabung mutex. Tiga sumber jawaban — satu thread, gabungan slot, dan statistik terkunci — harus bertemu di angka yang sama. ujiParalelDenganPembagianLain mengulangi pengecekan yang serupa dengan 3 thread dan 7 thread; bila pembagian benar, jumlah akhir tidak boleh peduli berapa pemotong yang dipakai. Keseluruhan harness berjalan sekejap saja karena 100.000 elemen sudah cukup untuk membuktikan kebenaran; pengujian bukan tempat mengukur performa.
Membaca Pengukuran Performa #
Sisa pertanyaannya dijawab mode --waktu: program menjalankan kedua versi dengan std::chrono::steady_clock — jam yang dirancang khusus untuk mengukur durasi, bukan waktu dinding — lalu mencetak milidetik masing-masing. Nilainya wajar dan bermanfaat sebagai gambaran, tetapi jangan pernah dijadikan output terverifikasi: waktu berbeda antar mesin, antar beban sistem, bahkan antar dua eksekusi beruntun. Di mesin uji dua core ini, empat thread tidak akan pernah memberi percepatan empat kali lipat penuh — selain jumlah core yang terbatas, ada biaya pembuatan thread dan sinkronisasi yang ikut dibayar. Angka yang stabil satu-satunya dari program ini tetaplah hasil perhitungannya, dan semuanya sudah dijaga harness.
Proyek multithreaded menutup rangkaian proyek di section ini. Empat bagian meninggalkan satu pola yang bisa dibawa ke program paralel mana pun: pecah pekerjaan menjadi rentang yang bisa diverifikasi, beri tiap thread slot hasil miliknya, kunci hanya data yang benar-benar dibagi, lalu uji hasilnya terhadap versi berurutan — bukan urutan eksekusinya. Kalau ingin menyelami teori di balik semua itu — model memori, memory ordering, atomic, dan struktur data bebas kunci — section Concurrency dan Parallelism membahasnya tuntas setelah rangkaian proyek ini.
Rujukan resmi: std::chrono::steady_clock (cppreference) , std::jthread (cppreference) .