[ESP32] 8. Giao tiếp giữa các tác vụ trong FreeRTOS

Arduino 26 Th07 2024

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à QueuesTask-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.

Tags

Tony Phạm

Là một người thích vọc vạch và tò mò với tất cả các lĩnh vực từ khoa học tự nhiên, lập trình, thiết kế đến ... triết học. Luôn mong muốn chia sẻ những điều thú vị mà bản thân khám phá được.