Giới thiệu nhanh
Hello các bạn! Ở bài trước về Semaphore, mình đã nhắc đến rằng có một cơ chế khác dùng để đồng bộ và bảo vệ tài nguyên dùng chung, đó chính là Mutex. Mình cũng nói rằng Mutex có sự khác biệt nhất định so với Semaphore, nhất là khi nói đến việc bảo vệ tài nguyên – thứ mà ta gặp rất nhiều khi làm việc với FreeRTOS hay bất kỳ hệ điều hành thời gian thực nào.
Vậy, hôm nay tụi mình sẽ cùng đi sâu vào Mutex là gì, sự khác biệt với Semaphore ở đâu, khi nào nên dùng Mutex thay vì Semaphore, và cách sử dụng Mutex sao cho đúng.
1. Mutex là gì?
Mutex là viết tắt của Mutual Exclusion – tức là “loại trừ lẫn nhau”. Hiểu đơn giản, đây là một kỹ thuật đảm bảo rằng chỉ một task duy nhất được quyền truy cập một tài nguyên tại một thời điểm.
Tài nguyên ở đây có thể là:
- Một biến dùng chung giữa các task
- Một thiết bị ngoại vi (ví dụ UART)
- Một hàm xử lý không reentrant (không an toàn khi gọi đồng thời)
2. Mutex hoạt động như thế nào?
Về cơ bản, Mutex hoạt động gần giống như Binary Semaphore. Một task có thể take
Mutex, và sau khi dùng xong tài nguyên, task đó phải give
lại Mutex để task khác có thể truy cập.
Tuy nhiên, Mutex có hai điểm khác biệt quan trọng:
a. Mutex có khái niệm về “chủ sở hữu”
Chỉ task nào take
Mutex thành công mới có quyền give
nó. Đây là điểm khác biệt lớn nhất với Semaphore, nơi bất kỳ task nào cũng có thể give
.
b. Mutex hỗ trợ Priority Inheritance (thừa kế ưu tiên)
Giả sử task A (ưu tiên thấp) giữ Mutex, và task B (ưu tiên cao) đang đợi Mutex đó. Trong trường hợp này, để tránh priority inversion (đảo ngược ưu tiên), FreeRTOS sẽ tạm thời nâng ưu tiên của task A lên ngang bằng task B để Mutex được trả nhanh hơn.
3. Minh họa cơ bản về Mutex
Cùng xem sơ đồ sau để hiểu cách Mutex hoạt động giữa các task:
+-----------+ +-----------+ +-----------+
| Task Low | | Task Med | | Task High |
| (prio 1) | | (prio 2) | | (prio 3) |
+-----------+ +-----------+ +-----------+ | | | |--- take Mutex ----> | |<-- Mutex acquired -- | | ... (đang dùng tài nguyên) | | | | | |--- ready -------->| | | | | Task High muốn take Mutex | |<----- blocked trên Mutex -------------| | Priority Inheritance kicks in | | Task Low tạm nâng lên prio 3 | | Task Low kết thúc sử dụng | |---- give Mutex ---------------------> | | Task High tiếp tục chạy |
✅ Kết luận từ sơ đồ:
- Nếu dùng Semaphore: Task High có thể bị chờ rất lâu nếu Task Low không trả Semaphore đúng lúc.
- Với Mutex: FreeRTOS hỗ trợ priority inheritance để giảm thời gian chờ đợi không cần thiết.
4. Cách dùng Mutex trong FreeRTOS
FreeRTOS cung cấp API cho Mutex như sau:
Tạo Mutex:
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
Task sử dụng Mutex:
void Task1(void *pvParameters)
{ for(;;) { if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) { // Dùng tài nguyên // ... xSemaphoreGive(xMutex); } }
}
Mutex cũng có phiên bản gọi là Recursive Mutex nếu bạn cần cho phép một task take
mutex nhiều lần liên tiếp (dạng đệ quy):
xSemaphoreCreateRecursiveMutex();
5. Mutex vs Semaphore – Phân biệt cụ thể
Tiêu chí | Mutex | Binary Semaphore |
---|---|---|
Mục đích chính | Bảo vệ tài nguyên dùng chung | Đồng bộ giữa các task/ISR |
Ai được give |
Chỉ task đã take mới give được |
Bất kỳ task/ISR nào |
Hỗ trợ priority inheritance | Có | Không |
Gọi từ ISR | Không | Có thể |
Được sử dụng như Recursive | Có thể (Recursive Mutex) | Không |
Trọng lượng bộ nhớ | Nặng hơn Semaphore một chút | Nhẹ hơn |
👉 Kết luận nhanh:
- Dùng Mutex để bảo vệ tài nguyên dùng chung (UART, I2C, biến toàn cục…).
- Dùng Binary Semaphore để đồng bộ giữa các task hoặc giữa ISR với task.
6. Một ví dụ thực tế: bảo vệ UART khi ghi từ nhiều task
Giả sử bạn có 3 task ghi log qua UART. Nếu không dùng Mutex, dữ liệu log sẽ bị trộn lẫn, vì các task ghi đồng thời.
void vLogTask(void *pvParam)
{ for(;;) { if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) { printf("Task %d: Hello\n", (int)pvParam); xSemaphoreGive(xMutex); } vTaskDelay(pdMS_TO_TICKS(500)); }
}
Trong main()
:
xMutex = xSemaphoreCreateMutex();
xTaskCreate(vLogTask, "Log1", 1024, (void*)1, 1, NULL);
xTaskCreate(vLogTask, "Log2", 1024, (void*)2, 1, NULL);
xTaskCreate(vLogTask, "Log3", 1024, (void*)3, 1, NULL);
7. Lưu ý khi dùng Mutex
✅ Không dùng trong ISR
Mutex không được thiết kế để dùng từ ISR vì nó không an toàn về mặt ngữ nghĩa (không có context để “nhớ” task nào đang sở hữu mutex). Nếu bạn cần đồng bộ từ ISR → Task, hãy dùng Binary Semaphore hoặc Direct To Task Notification.
✅ Nhớ give
sau khi take
Nếu bạn take
mà quên give
, mutex sẽ bị “giữ chặt” mãi mãi – gây deadlock cho các task khác.
✅ Tránh lạm dụng
Mutex chỉ nên dùng khi thực sự cần thiết. Nếu tài nguyên không dùng chung hoặc đã an toàn nhờ cách thiết kế khác, không cần thêm Mutex để tránh tốn CPU và RAM không cần thiết.
8. Tình huống nguy hiểm: Deadlock
Deadlock xảy ra khi hai task giữ Mutex mà mỗi task lại đợi Mutex của task kia. Ví dụ:
// Task A
xSemaphoreTake(Mutex1, portMAX_DELAY);
xSemaphoreTake(Mutex2, portMAX_DELAY); // bị kẹt nếu Mutex2 đang bị Task B giữ // Task B
xSemaphoreTake(Mutex2, portMAX_DELAY);
xSemaphoreTake(Mutex1, portMAX_DELAY); // bị kẹt nếu Mutex1 đang bị Task A giữ
Giải pháp:
- Quy định thứ tự
take
Mutex giống nhau ở mọi task. - Dùng timeout hợp lý.
- Dùng watchdog để phát hiện deadlock.
9. Khi nào nên dùng Recursive Mutex?
Nếu bạn viết một hàm có sử dụng Mutex và hàm đó lại được gọi từ một đoạn code khác đã take
mutex rồi, bạn sẽ cần một Recursive Mutex. Trường hợp này tương đối hiếm, nhưng rất quan trọng nếu xảy ra.
Ví dụ:
void foo() { xSemaphoreTakeRecursive(xMutex, portMAX_DELAY); // ... xSemaphoreGiveRecursive(xMutex);
} void bar() { xSemaphoreTakeRecursive(xMutex, portMAX_DELAY); foo(); // Không bị deadlock nếu dùng Recursive Mutex xSemaphoreGiveRecursive(xMutex);
}
10. Tổng kết
Đến đây, bạn đã hiểu rõ hơn về Mutex rồi đúng không? Mình tóm gọn lại vài ý:
✅ Mutex = Mutual Exclusion, dùng để bảo vệ tài nguyên dùng chung. ✅ Có hỗ trợ priority inheritance – khác với Semaphore. ✅ Chỉ task sở hữu mới được trả Mutex. ✅ Không dùng Mutex trong ISR. ✅ Có phiên bản Recursive Mutex cho các hàm gọi lồng nhau.
Mutex không chỉ là một phần quan trọng trong FreeRTOS, mà còn là một khái niệm nền tảng trong mọi hệ điều hành. Dùng đúng Mutex sẽ giúp hệ thống của bạn ổn định, tránh deadlock, và tối ưu hiệu suất khi nhiều task cùng truy cập một tài nguyên.