Biến volatile trong C

Trong lập trình C, từ khóa volatile
là một chỉ thị đặc biệt cho trình biên dịch, dùng để khai báo rằng giá trị của biến có thể bị thay đổi bất ngờ ngoài tầm kiểm soát của chương trình (ví dụ: bởi phần cứng, trình xử lý ngắt, hoặc các luồng khác trong hệ thống đa luồng).
Dưới đây là giải thích chi tiết:
🧠 Mục đích chính của volatile
volatile
được dùng để ngăn trình biên dịch tối ưu hóa các truy cập đến biến, vì trình biên dịch thường sẽ giả định rằng giá trị biến không thay đổi nếu không thấy đoạn mã nào thay đổi nó. Điều này có thể dẫn đến lỗi nếu biến thật sự bị thay đổi từ nơi khác.
Ví dụ điển hình:
volatile int flag;
while (flag == 0) {
// chờ flag đổi thành 1, ví dụ do ngắt hoặc thread khác
}
Nếu không có từ khóa volatile
, trình biên dịch có thể tối ưu hóa bằng cách đọc flag
một lần rồi dùng giá trị đó mãi, dẫn đến vòng lặp không bao giờ kết thúc – lỗi logic nghiêm trọng.
🛠️ Khi nào nên dùng volatile
?
Biến được cập nhật bởi phần cứng:
volatile int *status = (int *)0x1234; // Địa chỉ phần cứng while ((*status & READY_FLAG) == 0); // Chờ phần cứng sẵn sàng
Biến được cập nhật trong trình xử lý ngắt (interrupt handler):
volatile int interrupt_flag; void ISR() { interrupt_flag = 1; } int main() { while (interrupt_flag == 0); // chờ ngắt xảy ra }
Biến dùng giữa các thread (trong hệ thống không dùng cơ chế đồng bộ):
Lưu ý:
volatile
KHÔNG đảm bảo đồng bộ bộ nhớ (memory synchronization), chỉ đảm bảo không tối ưu hóa. Để đồng bộ thực sự, cần kết hợp với các công cụ như mutex, atomic operations, hoặcmemory barrier
.
⚠️ Những điều volatile
không làm:
Không giúp bảo vệ khỏi điều kiện race (race conditions).
Không đảm bảo tính nguyên tử (atomicity) của phép toán như
++
,--
,+=
.Không thay thế được cho các kỹ thuật đồng bộ hóa trong đa luồng (như mutex hoặc semaphore).
🧪 Ví dụ C với while
, dành cho ARM
🔸 Trường hợp 1 – KHÔNG có volatile
int flag = 0;
void wait_loop(void) {
while (flag == 0) {
// chờ
}
}
🔸 Trường hợp 2 – CÓ volatile
volatile int flag = 0;
void wait_loop(void) {
while (flag == 0) {
// chờ
}
}
🛠 Mã Assembly ARM (GCC, biên dịch với -O2
)
🔹 1. Không dùng volatile
(int flag
)
wait_loop:
ldr r3, =flag ; r3 ← địa chỉ biến flag
ldr r2, [r3] ; r2 ← *flag (đọc từ RAM)
.L2:
cmp r2, #0 ; so sánh r2 với 0
beq .L2 ; nếu bằng 0 thì lặp
bx lr ; trả về
📌 Phân tích:
flag
chỉ được đọc một lần duy nhất trước vòng lặp (→ldr r2, [r3]
).Trong vòng lặp
.L2
, chương trình chỉ kiểm tra lại biến đã lưu trong thanh ghir2
.Nếu
flag
được thay đổi từ ISR hoặc phần cứng → vòng lặp không thấy thay đổi, chương trình bị treo.
🔹 2. Có dùng volatile
(volatile int flag
)
wait_loop:
ldr r3, =flag ; r3 ← địa chỉ biến flag
.L3:
ldr r2, [r3] ; r2 ← *flag (đọc từ RAM mỗi lần)
cmp r2, #0 ; so sánh r2 với 0
beq .L3 ; nếu bằng 0 thì lặp
bx lr ; trả về
📌 Phân tích:
Mỗi lần lặp đều có
ldr r2, [r3]
→ luôn đọcflag
từ bộ nhớ thực.Đảm bảo mọi thay đổi từ bên ngoài đều được phát hiện.
Chính xác như kỳ vọng khi dùng trong hệ thống nhúng, ISR, hardware status polling.
📌 So sánh tóm tắt (ARM Assembly)
Đặc điểm | Không volatile | Có volatile |
ldr [r3] chỉ 1 lần | ✅ Có | ❌ Không |
Đọc lại trong mỗi vòng | ❌ Không | ✅ Có |
Theo dõi thay đổi ngoài | ❌ Sai | ✅ Đúng |
Rủi ro vòng lặp vô hạn | ✅ Có | ❌ Không |
🔧 Cách bạn có thể tự kiểm tra
Nếu bạn đang dùng Linux hoặc Raspberry Pi (ARM), bạn có thể dùng:
arm-none-eabi-gcc -O2 -S test.c -o test.s
Hoặc biên dịch trên Pi trực tiếp:
gcc -O2 -S test.c -o test.s
✅ Kết luận
Bạn muốn mình lấy thêm ví dụ về str
(ghi dữ liệu) hoặc for
, if
, hay ghi ra cả .map
hay objdump
để bạn xem chi tiết hơn không?
Subscribe to my newsletter
Read articles from Tony Vi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
