Topik ini membahas std::atomic, yaitu templat pustaka standar yang menjadikan operasi pada satu nilai berlangsung utuh tanpa terpotong oleh thread lain. Pembahasan mencakup penghitung tanpa mutex, bendera status antar thread, serta pengantar urutan memori (memory ordering). Contoh yang dipakai adalah penghitung kunjungan perpustakaan dengan empat thread, yaitu kasus yang sama dengan demo race condition, yang kini diselesaikan tanpa satu pun kunci yang diperoleh dan dilepaskan.
Operasi atomik pada satu nilai#
Sebuah objek std::atomic<T> menjamin setiap pembacaan dan penulisan terhadap nilai T berlangsung sebagai satu kesatuan. Operasi baca-modifikasi-tulis seperti fetch_add() juga dijamin utuh, sehingga langkah membaca, menaikkan, dan menulis ulang tidak dapat dipisahkan oleh thread lain. Jaminan ini disediakan langsung oleh instruksi prosesor bila tersedia, sedangkan method is_lock_free() melaporkan apakah operasinya benar-benar bebas kunci pada mesin yang menjalankannya. Tidak ada lock() maupun unlock() yang ditulis, sehingga pola ini lebih ringan daripada melindungi satu variabel dengan mutex.
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> pengunjung{0};
void catat_kunjungan() {
for (int i = 0; i < 25000; ++i) {
pengunjung.fetch_add(1);
}
}
int main() {
std::thread t1(catat_kunjungan);
std::thread t2(catat_kunjungan);
std::thread t3(catat_kunjungan);
std::thread t4(catat_kunjungan);
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << "total pengunjung = " << pengunjung.load() << "\n";
return 0;
}total pengunjung = 100000Penghitung pada program di atas bertipe std::atomic<int>, sehingga empat thread dapat menaikkannya tanpa mutex dan hasil akhirnya pasti 100000. Pemanggilan pengunjung.fetch_add(1) menaikkan nilai dan mengembalikan nilai sebelum kenaikan, sedangkan operator ++ dan += tersedia sebagai penulisan yang lebih ringkas untuk kasus yang sama. Pencetakan memakai load() secara eksplisit, dan dilakukan setelah seluruh join sehingga urutannya pasti. Tipe T yang diwadahi juga wajib trivially copyable, yaitu dapat disalin langsung sebagai rangkaian byte tanpa logika tambahan, sehingga int, bool, dan tipe pointer cocok, sedangkan tipe seperti std::string tidak dapat dibungkus. Keluaran ini sama pada setiap eksekusi, berbeda dengan demo tanpa pelindungan yang menghasilkan angka berubah-ubah.
Kompilasi dan eksekusi programnya memakai perintah berikut.
# kompilasi program dengan dukungan thread
g++ -std=c++17 -pthread penghitung-atomic.cpp -o /tmp/penghitung-atomic
# jalankan program hasil kompilasi
/tmp/penghitung-atomictotal pengunjung = 100000Nilai kembalian fetch_add() membuat pola pemberian nomor urut dapat ditulis tanpa kunci tambahan. Pemanggilan pertama mengembalikan 0, pemanggilan berikutnya mengembalikan 1, dan seterusnya, sehingga setiap thread memperoleh nomor yang berbeda tanpa menyentuh kunci. Pola ini dipakai pada antrean tiket loket, tempat setiap petugas memanggil nomor.fetch_add(1) untuk mendapatkan nomor tiket yang belum pernah terpakai. Method pendamping seperti fetch_sub() dan fetch_or() menyediakan pola yang sama untuk operasi hitung dan operasi bit.
Bendera atomik sebagai sinyal antar thread#
std::atomic<bool> sering dipakai sebagai bendera (flag), yaitu penanda status yang dibaca dan ditulis beberapa thread tanpa kunci. Bendera ini menjadi sinyal sederhana antar thread, misalnya untuk memberi tahu bahwa data sudah siap diproses. Program berikut memanfaatkannya untuk mengurutkan pekerjaan antara thread utama dan thread pekerja.
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<bool> data_siap{false};
std::atomic<int> total_batch{0};
void proses_batch(const std::vector<int>* nilai) {
// menunggu hingga data dinyatakan siap
while (!data_siap.load()) {
std::this_thread::yield();
}
int jumlah = 0;
for (int n : *nilai) {
jumlah += n;
}
total_batch.store(jumlah);
}
int main() {
std::vector<int> nilai = {80, 75, 90, 65};
std::thread pekerja(proses_batch, &nilai);
data_siap.store(true);
pekerja.join();
std::cout << "total batch nilai ujian = " << total_batch.load() << "\n";
return 0;
}total batch nilai ujian = 310Thread pekerja menunggu dalam perulangan yang memeriksa data_siap, dan method yield() mengalihkan penjadwalan sementara agar thread lain mendapat giliran prosesor. Thread utama mengisi vektor, menetapkan data_siap ke true, kemudian menunggu pekerja dengan join. Total yang dicetak pasti 310 karena penulisan total_batch baru terjadi setelah bendera terbaca true.
Urutan default pada store() dan load() ikut bekerja di sini. Penulisan data_siap dilakukan melalui store(), dan pembacaannya baru terjadi setelah bendera terlihat bernilai true, sehingga isi vektor yang sudah diisi terjamin terlihat oleh pekerja tanpa kunci tambahan. Bendera atomik juga lazim dipakai untuk menyampaikan permintaan berhenti, yaitu perulangan pekerja memeriksa bendera pada setiap putaran dan keluar bila nilainya berubah. Pola tersebut sederhana dan aman karena komunikasinya satu arah dan hanya menyentuh satu variabel.
Pola menunggu seperti ini memanggil load() terus-menerus, sehingga memakan giliran prosesor selama menunggu. Untuk penantian yang berlangsung lama, pustaka standar menyediakan std::condition_variable yang membuat thread benar-benar tidur hingga diberi sinyal.
Pengantar memory ordering#
Setiap operasi atomik menerima parameter std::memory_order yang mengatur keterlihatan urutan memori bagi thread lain. Nilai default std::memory_order_seq_cst memberi jaminan terkuat, yaitu seluruh thread melihat urutan operasi atomik yang sama, dan kedua program di atas membiarkan default ini berlaku. Nilai lain seperti std::memory_order_relaxed hanya menjamin keutuhan operasinya tanpa aturan urutan, sedangkan pasangan std::memory_order_acquire dan std::memory_order_release mengatur keterlihatan data yang ditulis sebelum operasi pelepasannya. Kombinasi yang lebih lemah menurunkan biaya pada sebagian arsitektur, tetapi penalarannya membutuhkan kehati-hatian karena kesalahannya jarang muncul dan sulit dilacak. Selama belum ada pengukuran kinerja yang menunjukkan perlunya, membiarkan nilai default adalah pilihan yang aman.
Kesalahan umum#
- Meneruskan objek
std::atomicsebagai salinan. Templat ini tidak dapat disalin maupun dipindahkan, sehingga compiler menolak kodenya. Teruskan lewat referensi atau pointer seperti pada program bendera. - Menganggap operasi atomik membuat seluruh fungsi aman. Jaminannya berlaku per operasi, sehingga logika yang menyentuh beberapa variabel sekaligus tetap perlu mutex.
- Menunggu bendera tanpa
yield()atau jeda. Perulangan yang hanya memeriksa bendera tanpa memberi kelonggaran memakan seluruh giliran prosesornya dan dapat memperlambat thread yang seharusnya menyetel bendera itu. - Memilih
memory_order_relaxeddemi kecepatan tanpa pengukuran. Kesalahan urutan memori muncul sporadis dan sulit direproduksi, sedangkan selisih kinerjanya sering tidak berarti untuk kasus umum.
Ringkasan#
std::atomic<T>menjadikan pembacaan, penulisan, dan operasi baca-modifikasi-tulis pada satu nilai berlangsung utuh tanpa mutex yang ditulis manual.fetch_add(),load(), danstore()adalah operasi dasarnya, sedangkanstd::atomic<bool>dipakai sebagai bendera sinyal antar thread.- Parameter
std::memory_ordermengatur keterlihatan urutan memori, dengan defaultseq_cstyang paling aman dan layak dipertahankan sebelum ada kebutuhan terukur. - Atomic melengkapi, bukan menggantikan, mutex, karena jaminannya berlaku pada satu operasi dan satu variabel saja.
Referensi#
- std::atomic — cppreference — Dokumentasi resmi templat atomik beserta operasi-operasi dasarnya.
- std::atomic
::fetch_add — cppreference — Aturan operasi penambahan atomik dan nilai yang dikembalikannya. - std::memory_order — cppreference — Daftar urutan memori dan jaminan masing-masing nilainya.
- std::this_thread::yield — cppreference — Perilaku penyerahan giliran prosesor yang dipakai pada perulangan menunggu bendera.