Bagian 2 sengaja meninggalkan satu titik belum dibedah: diAtasAmbang, satu-satunya data dalam jalankanParalel yang ditulis oleh lebih dari satu thread. Sisanya — data input yang hanya dibaca dan slot parsial yang masing-masing milik satu thread — aman tanpa pelindung. Titik tunggal itulah pintu masuk pelajaran terpenting proyek ini: begitu dua thread memodifikasi satu memori yang sama tanpa pengaturan, programnya tidak hanya menghasilkan angka yang salah; menurut standar C++, seluruh programnya menjadi undefined behavior — compiler bebas mengasumsikan kondisi itu tidak pernah terjadi. Bagian ini menunjukkan tabrakan itu secara nyata, lalu memperbaikinya dengan pasangan klasik std::mutex dan std::lock_guard.
Satu Variable, Banyak Penulis #
Potongan berikut dari src/multithread.cpp memperlihatkan pembagian peran antara data yang aman dan data yang harus dijaga:
// 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;
});
}Kenapa diAtasAmbang += kontribusi; berbahaya sedangkan parsial[i] = ...; tidak? += bukan satu operasi mesin, melainkan tiga: baca isi variable, tambahkan, tulis kembali. Bila thread A membaca 100 lalu thread B membaca 100 sebelum salah satunya selesai menulis, kedua thread menulis 101 dan satu kontribusi hilang begitu saja — kejadian yang disebut lost update. Semakin sering program dijalankan, semakin tidak bisa ditebak berapa banyak yang hilang, karena penjadwalan thread berbeda di setiap eksekusi.
Baris demi baris mekanisme pelindungnya:
std::int64_t diAtasAmbang = 0;: data bersama yang benar-benar ditulis semua thread. Segala sesuatu yang berstatus “ditulis oleh banyak pihak” wajib kita tunjuk secara eksplisit — jangan sampai tersembunyi di dalam function.std::mutex kunciStatistik;: mutex (mutual exclusion) bukan data dan tidak berisi apa pun; ia hanya alat pemberian giliran. Hanya satu thread yang boleh menguncikunciStatistikpada satu waktu; thread lain yang mencoba akan menunggu.kontribusidihitung di luar kunci: pekerjaan berat — menyapu ribuan elemen — dikerjakan tanpa memegang kunci. Kunci hanya melindungi penambahan yang selesai dalam hitungan nanodetik. Memegang kunci selama kerja berat membatalkan paralelisme itu sendiri, karena thread lain antre hanya untuk menunggu.std::lock_guard<std::mutex> penjaga(kunciStatistik);: kunci diambil saatpenjagadibuat dan dilepas otomatis saatpenjagakeluar scope — pola RAII yang kita pakai sejak proyek-proyek awal. Bila blok ini keluar lewat exception sekalipun, kunci tetap terlepas; tidak ada jalur yang meninggalkan mutex terkunci selamanya.diAtasAmbang += kontribusi;: tiga langkah read-modify-write kini berlangsung utuh dalam satu giliran. Nilai yang dibaca tidak mungkin basi, karena tidak ada thread lain yang boleh masuk sebelumpenjagabubar.
Melihat Tabrakan Tanpa Mutex #
Untuk menghargai pelindung itu, kita buka dulu kejadiannya tanpa pelindung. Program berikut sengaja memuat data race — jangan meniru polanya:
// demo-race.cpp — versi TANPA mutex untuk melihat lost update.
// PERINGATAN: program ini sengaja mengandung data race (undefined behavior).
// Hasilnya tidak bisa dijadikan output terverifikasi — nilainya berubah-ubah.
#include <cstdint>
#include <print>
#include <thread>
#include <vector>
int main() {
constexpr int JUMLAH_THREAD = 4;
constexpr int PUTARAN = 100'000;
std::int64_t penghitung = 0; // tanpa pelindung!
std::vector<std::jthread> pekerja;
for (int id = 0; id < JUMLAH_THREAD; ++id) {
pekerja.emplace_back([&] {
for (int i = 0; i < PUTARAN; ++i) {
penghitung += 1;
}
});
}
for (std::jthread& t : pekerja) {
t.join();
}
std::println("diharapkan : {}",
static_cast<std::int64_t>(JUMLAH_THREAD) * PUTARAN);
std::println("hasil : {}", penghitung);
return 0;
}Karena program ini mengandung data race, hasilnya tidak bisa dijadikan output terverifikasi. Salah satu eksekusi nyata di mesin uji kita memberi:
diharapkan : 400000
hasil : 261608Empat thread seharusnya menambah 400.000, tetapi hasilnya 261.608 — hampir 140.000 penambahan hilang — dan angka itu berbeda di setiap eksekusi. Tidak ada crash, tidak ada pesan galat; programnya lulus dengan senyap sambil membawa jawaban yang salah. Kegagalan jenis ini jauh lebih mahal daripada program yang berhenti dengan error, karena hanya pemeriksaan yang menangkapnya.
Sekarang versi amannya, demo-mutex.cpp, dengan pola yang sama seperti pada jalankanParalel:
// demo-mutex.cpp — statistik bersama yang dijaga mutex.
// Empat jthread menambah satu variable yang sama sebanyak 100.000 kali.
// Bagian penambahannya dilindungi std::mutex, sehingga hasil akhir selalu
// tepat — bukan angka salah akibat update yang saling menimpa.
#include <cstdint>
#include <mutex>
#include <print>
#include <thread>
#include <vector>
int main() {
constexpr int JUMLAH_THREAD = 4;
constexpr int PUTARAN = 100'000;
std::int64_t penghitung = 0;
std::mutex kunci;
std::vector<std::jthread> pekerja;
for (int id = 0; id < JUMLAH_THREAD; ++id) {
pekerja.emplace_back([&] {
for (int i = 0; i < PUTARAN; ++i) {
const std::lock_guard<std::mutex> penjaga(kunci);
penghitung += 1;
}
});
}
for (std::jthread& t : pekerja) {
t.join();
}
std::println("diharapkan : {}",
static_cast<std::int64_t>(JUMLAH_THREAD) * PUTARAN);
std::println("hasil : {}", penghitung);
return 0;
}g++ -std=c++23 -Wall -Wextra -pthread demo-mutex.cpp -o demo-mutex
./demo-mutexOutput yang Dihasilkan:
diharapkan : 400000
hasil : 400000Kali ini hasilnya selalu tepat 400000, di eksekusi kapan pun dan di mesin mana pun. Biayanya juga terlihat jelas: setiap penambahan harus antre di kunci, sehingga bagian ini berjalan praktis berurutan. Karena itu aturan desainnya bukan “kunci semuanya”, melainkan kebalikannya — susun agar data tidak dibagi bila bisa. Program kita mengikuti aturan itu: jutaan operasi penjumlahan berjalan bebas di slot pribadi masing-masing, dan hanya satu angka statistik yang membutuhkan antrean.
Dengan data bersama yang kini terlindungi, seluruh mekanisme program paralel kita sudah berdiri: pembagian rentang, eksekusi jthread, dan sinkronisasi yang tepat sasaran. Yang tersisa adalah pertanyaan yang selalu menyertai program paralel — apakah semua ini benar dan apakah semua ini lebih cepat — dan itulah yang kita jawab dengan harness pengujian di Bagian 4.
Rujukan resmi: std::mutex (cppreference) , std::lock_guard (cppreference) .