0

Các Vấn Đề Thường Gặp Khi Dùng Mutex & Giải Pháp Với Gatekeeper Task


1. Giới thiệu về Mutex

1.1 Mutex là gì?

Trong FreeRTOS, Mutex (Mutual Exclusion) là một dạng Binary Semaphore đặc biệt được thiết kế nhằm bảo vệ tài nguyên dùng chung như UART, I2C, LCD, biến toàn cục... chỉ cho một task truy cập tại một thời điểm.

1.2 Khác biệt với Semaphore thường

  • Priority Inversion: Mutex hỗ trợ cơ chế priority inheritance, đảm bảo task có độ ưu tiên thấp đang giữ mutex sẽ được nâng tạm thời lên bằng với task ưu tiên cao đang đợi mutex.
  • Ownership: Chỉ task nào lấy mutex thành công mới có quyền trả lại (release) mutex. Semaphore thường thì không yêu cầu điều này.

2. Các vấn đề thường gặp khi sử dụng Mutex

2.1 Deadlock

Deadlock xảy ra khi hai hoặc nhiều task cùng chờ nhau giải phóng mutex mà không ai nhả mutex ra, khiến hệ thống treo vĩnh viễn.

Ví dụ minh họa:

// Task A
xSemaphoreTake(mutexA, portMAX_DELAY);
xSemaphoreTake(mutexB, portMAX_DELAY);  // bị block ở đây nếu mutexB đã bị chiếm

// Task B
xSemaphoreTake(mutexB, portMAX_DELAY);
xSemaphoreTake(mutexA, portMAX_DELAY);  // bị block ở đây nếu mutexA đã bị chiếm

2.2 Priority Inversion (Đảo ngược ưu tiên)

Khi một task có độ ưu tiên thấp giữ mutex, trong khi task ưu tiên cao đang bị block vì mutex đó, và một task ưu tiên trung bình liên tục chạy, task cao sẽ không được chạy mặc dù có độ ưu tiên cao hơn.

→ Task C bị chậm tiến độ, mặc dù có độ ưu tiên cao nhất.

2.3 Quên trả mutex

Nếu task lấy mutex nhưng exit sớm hoặc lỗi logic khiến không gọi xSemaphoreGive(), mutex sẽ không bao giờ được nhả, dẫn tới các task khác bị block vĩnh viễn.

2.4 Dùng mutex sai ngữ cảnh

Gọi xSemaphoreTake() trong ISR (ngắt) hoặc trong các đoạn code không phải task context sẽ dẫn tới lỗi nghiêm trọng.


3. Giải pháp: Gatekeeper Task

3.1 Gatekeeper Task là gì?

Gatekeeper Task là một thiết kế giúp loại bỏ hoàn toàn nhu cầu mutex trong các trường hợp như:

  • Ghi log UART
  • Truy xuất LCD
  • Truy xuất bộ nhớ không an toàn

Nguyên tắc hoạt động: tạo ra một task chuyên phục vụ việc thao tác tài nguyên dùng chung. Các task khác gửi request qua Queue đến Gatekeeper, và task này xử lý tuần tự.


3.2 Sơ đồ minh họa Gatekeeper Task

image.png


4. Code minh họa Gatekeeper Task với UART

4.1 Định nghĩa

  • Dùng queue để gửi chuỗi cần in ra UART.
  • Chỉ Gatekeeper Task mới gọi HAL_UART_Transmit().

4.2 Cấu hình

#define QUEUE_LENGTH     10
#define MAX_STRING_LEN   50
QueueHandle_t uartQueue;

4.3 Task Gatekeeper

void UARTGatekeeperTask(void *param) {
    char message[MAX_STRING_LEN];

    while (1) {
        if (xQueueReceive(uartQueue, message, portMAX_DELAY) == pdPASS) {
            HAL_UART_Transmit(&huart2, (uint8_t*)message, strlen(message), HAL_MAX_DELAY);
        }
    }
}

4.4 Các task khác gửi message

void SensorTask(void *param) {
    char msg[MAX_STRING_LEN];

    while (1) {
        sprintf(msg, "Sensor Value: %d\r\n", read_sensor());
        xQueueSend(uartQueue, msg, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

5. So sánh: Mutex vs Gatekeeper Task

Tiêu chí Mutex Gatekeeper Task
Cần priority inheritance? Không cần
Đơn giản trong code? Có vẻ đơn giản hơn Phức tạp hơn chút
Khả năng mở rộng Giới hạn khi task nhiều Mạnh mẽ, dễ mở rộng
Gây deadlock? Có thể Không
Gây priority inversion? Có thể Không
Dễ debug? Khó khi bị lỗi liên quan ưu tiên Dễ quan sát behavior

6. Khi nào nên dùng Gatekeeper?

Nên dùng Gatekeeper khi:

  • Tài nguyên có giới hạn và không thread-safe (UART, LCD, EEPROM).
  • Nhiều task cùng truy cập.
  • Cần tránh priority inversion hoàn toàn.
  • Muốn hệ thống dễ debug và theo dõi.

Không nên dùng Gatekeeper khi:

  • Tài nguyên rất nhỏ và cần truy cập cực nhanh (dưới vài microsecond).
  • Hệ thống đơn giản, ít task.

7. Mở rộng Gatekeeper Task: Gửi kèm metadata

Giả sử bạn muốn gửi log theo dạng:

typedef struct {
    char content[MAX_STRING_LEN];
    uint32_t timestamp;
    uint8_t level; // 0: Info, 1: Warn, 2: Error
} LogMessage;

Task gửi message

LogMessage log;
log.level = 1;
log.timestamp = xTaskGetTickCount();
strcpy(log.content, "Temperature too high!");

xQueueSend(uartQueue, &log, portMAX_DELAY);

Task Gatekeeper xử lý format

void UARTGatekeeperTask(void *param) {
    LogMessage log;

    while (1) {
        if (xQueueReceive(uartQueue, &log, portMAX_DELAY) == pdPASS) {
            char formatted[100];
            sprintf(formatted, "[%lu][%s] %s\r\n", 
                log.timestamp,
                log.level == 0 ? "INFO" : log.level == 1 ? "WARN" : "ERROR",
                log.content);

            HAL_UART_Transmit(&huart2, (uint8_t*)formatted, strlen(formatted), HAL_MAX_DELAY);
        }
    }
}

8. Kết luận

Việc dùng Mutex là rất phổ biến trong các hệ thống FreeRTOS, nhưng không phải lúc nào cũng an toàn và tối ưu. Những lỗi như deadlock, priority inversion, hoặc quên nhả mutex có thể khiến hệ thống không ổn định, khó debug.

Trong những trường hợp truy xuất tài nguyên như UART, EEPROM, SPI hoặc LCD, thay vì dùng mutex, thiết kế với Gatekeeper Task có thể giúp:

  • Đơn giản hóa cấu trúc hệ thống
  • Tránh các lỗi liên quan ưu tiên
  • Tăng độ ổn định và mở rộng dễ dàng

Với chiến lược đúng, bạn có thể tạo ra một hệ thống FreeRTOS vững chắc, dễ bảo trì và có khả năng mở rộng cao cho các ứng dụng thực tế.



All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí