[ESP32] 10. Kỹ thuật lập trình đa nhân nâng cao
Kỹ thuật lập trình đa nhân nâng cao đòi hỏi hiểu biết sâu sắc về các vấn đề phức tạp có thể xảy ra trong hệ thống đa tác vụ và cách giải quyết chúng. Việc áp dụng đúng các kỹ thuật này sẽ giúp bạn xây dựng các ứng dụng đa tác vụ ổn định, hiệu quả trên ESP32. Các kỹ thuật lập trình này cũng đòi hỏi bạn cần có sự thực hành và trải nghiệm để rút ra cách xử lý phù hợp tùy vào hoàn cảnh.
10.1. Deadlocks và cách tránh
Giải thích chi tiết:
Deadlock là tình huống hai hoặc nhiều tác vụ bị chặn vĩnh viễn, mỗi tác vụ chờ đợi tài nguyên mà tác vụ khác đang giữ.
Nguyên nhân:
- Mutual Exclusion: Tài nguyên không thể được chia sẻ.
- Hold and Wait: Tác vụ giữ tài nguyên trong khi chờ đợi tài nguyên khác.
- No Preemption: Tài nguyên không thể bị thu hồi từ tác vụ.
- Circular Wait: Có chu trình các tác vụ chờ đợi lẫn nhau.
Cách tránh:
- Đặt thứ tự cố định cho việc yêu cầu tài nguyên.
- Sử dụng timeout khi yêu cầu tài nguyên.
- Sử dụng kỹ thuật "trylock" thay vì "lock" truyền thống.
Ví dụ:
SemaphoreHandle_t xSemaphore1, xSemaphore2;
// Cách có thể dẫn đến deadlock
void taskDeadlockProne(void *pvParameters) {
while(1) {
xSemaphoreTake(xSemaphore1, portMAX_DELAY);
xSemaphoreTake(xSemaphore2, portMAX_DELAY);
// Sử dụng tài nguyên
xSemaphoreGive(xSemaphore2);
xSemaphoreGive(xSemaphore1);
}
}
// Cách tránh deadlock
void taskDeadlockSafe(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xSemaphore1, 1000 / portTICK_PERIOD_MS) == pdTRUE) {
if (xSemaphoreTake(xSemaphore2, 1000 / portTICK_PERIOD_MS) == pdTRUE) {
// Sử dụng tài nguyên
xSemaphoreGive(xSemaphore2);
}
xSemaphoreGive(xSemaphore1);
}
// Xử lý trường hợp không lấy được semaphore
}
}
10.2. Race conditions và cách xử lý
Giải thích chi tiết:
Race condition xảy ra khi hai hoặc nhiều tác vụ cố gắng truy cập và sửa đổi dữ liệu chia sẻ cùng một lúc, dẫn đến kết quả không xác định.
Cách xử lý:
- Sử dụng mutexes để bảo vệ dữ liệu chia sẻ.
- Sử dụng atomic operations khi có thể.
- Thiết kế cẩn thận để giảm thiểu việc chia sẻ dữ liệu giữa các tác vụ.
Ví dụ:
SemaphoreHandle_t xMutex;
volatile int sharedCounter = 0;
// Có thể dẫn đến race condition
void taskUnsafe(void *pvParameters) {
while(1) {
sharedCounter++;
vTaskDelay(1);
}
}
// An toàn với race condition
void taskSafe(void *pvParameters) {
while(1) {
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
sharedCounter++;
xSemaphoreGive(xMutex);
}
vTaskDelay(1);
}
}
10.3. Priority Inversion và Priority Inheritance
Giải thích chi tiết:
Priority Inversion xảy ra khi một tác vụ ưu tiên cao bị chặn bởi một tác vụ ưu tiên thấp đang giữ một tài nguyên chung.
Priority Inheritance là một kỹ thuật trong đó tác vụ ưu tiên thấp tạm thời kế thừa mức ưu tiên của tác vụ ưu tiên cao đang chờ đợi tài nguyên mà nó đang giữ.
Cách xử lý:
FreeRTOS tự động hỗ trợ Priority Inheritance cho mutexes.
Ví dụ:
SemaphoreHandle_t xMutex;
void lowPriorityTask(void *pvParameters) {
while(1) {
xSemaphoreTake(xMutex, portMAX_DELAY);
// Thực hiện công việc với tài nguyên được bảo vệ
vTaskDelay(1000 / portTICK_PERIOD_MS);
xSemaphoreGive(xMutex);
}
}
void mediumPriorityTask(void *pvParameters) {
while(1) {
// Thực hiện một số công việc
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
void highPriorityTask(void *pvParameters) {
while(1) {
xSemaphoreTake(xMutex, portMAX_DELAY);
// Thực hiện công việc quan trọng với tài nguyên được bảo vệ
xSemaphoreGive(xMutex);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
void setup() {
xMutex = xSemaphoreCreateMutex();
xTaskCreate(lowPriorityTask, "Low", 1000, NULL, 1, NULL);
xTaskCreate(mediumPriorityTask, "Medium", 1000, NULL, 2, NULL);
xTaskCreate(highPriorityTask, "High", 1000, NULL, 3, NULL);
}
Trong ví dụ này, khi tác vụ ưu tiên cao cần mutex, tác vụ ưu tiên thấp đang giữ mutex sẽ tạm thời được nâng lên mức ưu tiên cao, ngăn tác vụ ưu tiên trung bình can thiệp.
Lưu ý quan trọng:
- Luôn thiết kế hệ thống để giảm thiểu khả năng xảy ra deadlock và race conditions.
- Sử dụng các công cụ phân tích và debug để phát hiện các vấn đề tiềm ẩn.
- Kiểm tra kỹ lưỡng hệ thống trong các điều kiện tải cao và đa dạng.
- Hiểu rõ cách FreeRTOS xử lý ưu tiên và lập lịch tác vụ để tận dụng tối đa các tính năng của nó.