volatile adalah type qualifier yang memberi tahu compiler bahwa sebuah objek dapat berubah di luar alur eksekusi normal program. Tanpa volatile, compiler bebas mengoptimalkan akses: menyimpan nilai di register, menghapus loop yang dianggap tidak berguna, atau menggabungkan beberapa akses menjadi satu. Dengan volatile, compiler dipaksa untuk selalu membaca dari memori pada setiap akses dan tidak menghapus akses meskipun tampak tidak berguna. Pada chapter ini kita akan memakai volatile untuk dua skenario klasik: flag yang diubah oleh signal handler, dan register memory-mapped IO.
Skenario 1: Flag dari Signal Handler #
Saat kita menerima sinyal asinkron (misalnya SIGINT dari Ctrl+C), kernel menginterupsi program dan memanggil signal handler di tengah eksekusi. Handler biasanya hanya menset flag “berhenti” yang diperiksa di main loop. Tanpa volatile, compiler melihat while (!stop_flag) { ... } dan menganggap stop_flag tidak pernah berubah di dalam loop (karena loop tidak mengubahnya), lalu mengoptimalkan loop menjadi infinite loop tanpa membaca ulang stop_flag.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/* volatile sig_atomic_t untuk flag dari signal handler */
static volatile sig_atomic_t stop_flag = 0;
static void handler(int sig) {
(void)sig;
stop_flag = 1;
}
int main(void) {
signal(SIGINT, handler);
signal(SIGTERM, handler);
printf("mulai. kirim SIGINT (Ctrl+C) untuk menghentikan.\n");
printf("(simulasi: set stop_flag = 1 setelah 1 detik)\n");
/* Simulasi: set stop_flag setelah 1 detik */
sleep(1);
raise(SIGINT);
int count = 0;
while (!stop_flag) {
count++;
if (count > 1000000) break; /* safety */
}
printf("loop berhenti setelah %d iterasi (stop_flag=%d)\n",
count, (int)stop_flag);
return 0;
}Output yang Dihasilkan:
mulai. kirim SIGINT (Ctrl+C) untuk menghentikan.
(simulasi: set stop_flag = 1 setelah 1 detik)
loop berhenti setelah 0 iterasi (stop_flag=1)Mari kita telaah baris-baris kode di atas:
- Baris 7 (
static volatile sig_atomic_t stop_flag = 0;): tipesig_atomic_tadalah integer yang dapat dibaca/tulis secara atomik (tidak terpotong oleh signal).volatilememaksa compiler membaca ulangstop_flagdi setiap iterasi loop. Kombinasivolatile sig_atomic_tadalah idiom standar untuk flag signal handler. - Baris 9-12 (
handler): signal handler hanya menset flag—tidak boleh memanggil fungsi non-async-signal-safe sepertiprintfataumalloc. Set flag adalah operasi yang aman. - Baris 21 (
raise(SIGINT);): simulasi menerima sinyal denganraise()—setara dengan menekan Ctrl+C, tetapi deterministik. - Baris 24 (
while (!stop_flag) { ... }): denganvolatile, compiler membacastop_flagdari memori di setiap iterasi. Tanpavolatile, compiler akan meng-cachestop_flagke register saat loop dimulai dan loop tidak pernah keluar. - Baris 25-27 (safety counter): batas iterasi sebagai jaring pengaman bila kontrak
volatilegagal pada compiler tertentu. Pada compiler standar modern, batas ini tidak pernah tersentuh.
Skenario 2: Memory-Mapped IO #
Pada pemrograman kernel, driver, atau mikrokontroler, kita sering berinteraksi dengan register perangkat keras yang dipetakan ke alamat memori tertentu. Membaca alamat tersebut mengembalikan nilai register, menulis mengubah konfigurasi perangkat. Compiler tidak boleh mengoptimalkan akses ini: setiap read/write harus benar-benar dikirim ke perangkat keras.
#include <stdio.h>
#include <stdint.h>
/* Simulasi memory-mapped IO: alamat "register" palsu */
static uint32_t fake_register = 0;
/* volatile menandai: baca ulang setiap kali, jangan cache */
uint32_t read_reg(volatile uint32_t *reg) {
return *reg;
}
void write_reg(volatile uint32_t *reg, uint32_t val) {
*reg = val;
}
/* Demo: tanpa volatile, compiler akan cache nilai */
uint32_t read_normal(uint32_t *reg) {
return *reg;
}
int main(void) {
volatile uint32_t *reg = &fake_register;
write_reg(reg, 0x12345678);
printf("baca pertama: 0x%08X\n", read_reg(reg));
write_reg(reg, 0xDEADBEEF);
printf("baca kedua: 0x%08X\n", read_reg(reg));
/* Tunjukkan volatile mencegah optimasi */
/* Compiler akan melakukan dua read terpisah dengan volatile */
uint32_t a = read_reg(reg);
uint32_t b = read_reg(reg);
printf("dua read volatile: a=0x%08X b=0x%08X (sama karena tidak ada perubahan)\n", a, b);
return 0;
}Output yang Dihasilkan:
baca pertama: 0x12345678
baca kedua: 0xDEADBEEF
dua read volatile: a=0xDEADBEEF b=0xDEADBEEF (sama karena tidak ada perubahan)Fungsi read_reg dan write_reg menerima volatile uint32_t *. Setiap panggilan read_reg menghasilkan instruksi load memori yang sebenarnya, tidak dioptimasi menjadi cache register. Pada kode produksi, reg akan mengarah ke alamat fisik perangkat keras seperti 0x40021000 (alamat GPIO STM32), bukan ke variabel biasa.
Kapan volatile Tidak Diperlukan #
Banyak pemrogram salah menggunakan volatile untuk hal-hal yang sebenarnya tidak membutuhkannya:
- Variabel yang diubah thread lain: gunakan
_Atomicatau mutex, bukanvolatile.volatiletidak mencegah data race. - Variabel yang “mungkin berubah”: gunakan
constdanmutableatau refactor—bukan urusan compiler. - Optimasi tertentu yang ingin dihindari: gunakan atribut compiler (
__attribute__((noinline))) atau barrier eksplisit.
volatile hanya relevan untuk (1) memory-mapped IO, (2) flag signal handler, dan (3) variabel yang dapat dimodifikasi oleh interupsi atau debugger.