Biến volatile trong C

Tony ViTony Vi
4 min read

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?

  1. 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
    
  2. 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
     }
    
  3. 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ặc memory 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 ghi r2.

  • 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 đọc flag 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ểmKhông volatilevolatile
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?

0
Subscribe to my newsletter

Read articles from Tony Vi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tony Vi
Tony Vi