[ESP32] 8. Giao tiếp giữa các tác vụ trong FreeRTOS
Trong lập trình đa luồng với FreeRTOS, việc giao tiếp an toàn và hiệu quả giữa các tác vụ là rất quan trọng. Dưới đây là chi tiết về hai phương pháp phổ biến là Queues và Task-to-Task Notifications.
8.1. Queues (Hàng đợi)
- Giá trị: Truyền dữ liệu an toàn giữa các tác vụ.
- Giải thích: Queues hoạt động như một bộ đệm, cho phép các tác vụ gửi và nhận dữ liệu theo cơ chế FIFO (First-In, First-Out).
Ưu điểm:
- An toàn: FreeRTOS đảm bảo tính toàn vẹn dữ liệu khi nhiều tác vụ truy cập vào cùng một Queue.
- Hiệu quả: Mô hình producer-consumer được hỗ trợ hiệu quả, nơi một tác vụ tạo dữ liệu (producer) và một tác vụ khác xử lý dữ liệu (consumer).
- Linh hoạt: Có thể lưu trữ nhiều kiểu dữ liệu khác nhau trong Queue.
Ví dụ:
// Tạo Queue với kích thước 10, mỗi phần tử là kiểu int
QueueHandle_t xQueue = xQueueCreate(10, sizeof(int));
// Tác vụ Producer
void taskProducer(void *pvParameters) {
int data = 0;
while (1) {
// Tạo dữ liệu
data++;
// Gửi dữ liệu vào Queue
xQueueSend(xQueue, &data, portMAX_DELAY);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// Tác vụ Consumer
void taskConsumer(void *pvParameters) {
int receivedData;
while (1) {
// Nhận dữ liệu từ Queue
if (xQueueReceive(xQueue, &receivedData, portMAX_DELAY) == pdTRUE) {
// Xử lý dữ liệu
Serial.println("Received data: " + String(receivedData));
}
}
}
8.2. Task-to-Task Notifications (Thông báo giữa các tác vụ)
- Giá trị: Giao tiếp nhanh và hiệu quả giữa các tác vụ.
- Giải thích: Task Notifications là một cơ chế lightweight cho phép gửi tín hiệu hoặc dữ liệu nhỏ (32-bit) giữa các tác vụ.
Ưu điểm:
- Nhanh chóng: Tiết kiệm tài nguyên và thời gian xử lý hơn so với Queues.
- Hiệu quả: Phù hợp cho việc đồng bộ hóa đơn giản hoặc gửi tín hiệu sự kiện.
Ví dụ:
TaskHandle_t task1Handle;
// Tác vụ 1
void task1(void *pvParameters) {
while (1) {
// Thực hiện công việc
// ...
// Gửi thông báo cho Task 2
xTaskNotifyGive(task2Handle);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// Tác vụ 2
void task2(void *pvParameters) {
while (1) {
// Chờ nhận thông báo
if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY)) {
// Xử lý khi nhận được thông báo
Serial.println("Notification received!");
}
}
}
8.3. Shared Memory (Bộ nhớ dùng chung)
- Giá trị: Chia sẻ dữ liệu lớn giữa các tác vụ.
- Giải thích: Shared Memory cho phép nhiều tác vụ truy cập trực tiếp vào cùng một vùng bộ nhớ. Điều này hữu ích cho việc chia sẻ dữ liệu lớn mà không cần sao chép, giúp tiết kiệm thời gian và tài nguyên hệ thống.
Ưu điểm:
- Hiệu suất: Truy cập và chia sẻ dữ liệu lớn nhanh chóng do không cần sao chép.
- Tiết kiệm bộ nhớ: Chỉ sử dụng một bản sao dữ liệu cho nhiều tác vụ.
Nhược điểm:
- Khó quản lý: Cần cơ chế đồng bộ hóa để tránh race condition khi nhiều tác vụ cùng truy cập và sửa đổi dữ liệu trong Shared Memory.
- Dễ xảy ra lỗi: Nếu không được bảo vệ cẩn thận, việc truy cập đồng thời có thể dẫn đến dữ liệu không nhất quán và gây ra lỗi khó kiểm soát.
Cần lưu ý:
- Đồng bộ hóa: Sử dụng các cơ chế đồng bộ hóa như mutex, semaphore, critical section để đảm bảo chỉ một tác vụ có thể truy cập và sửa đổi dữ liệu trong Shared Memory tại một thời điểm.
- Kiến trúc dữ liệu: Lựa chọn cấu trúc dữ liệu phù hợp để lưu trữ trong Shared Memory, tối ưu hóa việc truy cập và giảm thiểu xung đột.
Ví dụ:
// Khai báo biến toàn cục (Global variable) làm Shared Memory
int sharedData = 0;
SemaphoreHandle_t xMutex;
// Tác vụ 1
void task1(void *pvParameters) {
while (1) {
// Yêu cầu quyền truy cập Shared Memory
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// Đọc và sửa đổi dữ liệu trong Shared Memory
sharedData++;
// Giải phóng quyền truy cập Shared Memory
xSemaphoreGive(xMutex);
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
// Tác vụ 2
void task2(void *pvParameters) {
while (1) {
// Yêu cầu quyền truy cập Shared Memory
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// Đọc dữ liệu từ Shared Memory
Serial.println("Shared data: " + String(sharedData));
// Giải phóng quyền truy cập Shared Memory
xSemaphoreGive(xMutex);
}
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
void setup() {
// Khởi tạo Mutex
xMutex = xSemaphoreCreateMutex();
// ...
}
Kết luận:
- Sử dụng Queues khi cần truyền dữ liệu phức tạp hoặc dung lượng lớn giữa các tác vụ.
- Sử dụng Task Notifications cho việc giao tiếp nhanh chóng, đơn giản và tiết kiệm tài nguyên, ví dụ như gửi tín hiệu sự kiện hoặc đồng bộ hóa.
- Sử dụng Shared Memory để chia sẻ dữ liệu lớn giữa các tác vụ trong FreeRTOS. Tuy nhiên, cần thận trọng trong việc đồng bộ hóa và quản lý truy cập để tránh xung đột và đảm bảo tính nhất quán của dữ liệu.
Bằng cách lựa chọn phương pháp giao tiếp phù hợp, bạn có thể xây dựng các ứng dụng đa luồng hiệu quả và đáng tin cậy trên ESP32 với FreeRTOS.