[ESP32] 6. Đồng bộ hóa tác vụ

Arduino 25 Th07 2024

Đồng bộ hóa tác vụ là quá trình điều phối các tác vụ để đảm bảo chúng hoạt động một cách nhịp nhàng và an toàn khi truy cập vào tài nguyên chung. Trong FreeRTOS, có nhiều cơ chế đồng bộ hóa khác nhau. Hiểu và sử dụng đúng các cơ chế đồng bộ hóa này sẽ giúp bạn xây dựng các ứng dụng đa tác vụ an toàn và hiệu quả trên ESP32.

6.1. Semaphores

Giải thích chi tiết:
Semaphore là một cơ chế đồng bộ hóa cho phép kiểm soát truy cập vào tài nguyên chia sẻ hoặc đồng bộ hóa các sự kiện giữa các tác vụ.

FreeRTOS cung cấp hai loại semaphore chính:

  1. Binary Semaphore: Chỉ có hai trạng thái (available hoặc not available).
  2. Counting Semaphore: Có thể có nhiều hơn một "token" available.

Ví dụ:

SemaphoreHandle_t xSemaphore = NULL;

void senderTask(void *pvParameters) {
    while(1) {
        xSemaphoreGive(xSemaphore);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void receiverTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            Serial.println("Semaphore received");
        }
    }
}

void setup() {
    Serial.begin(115200);
    xSemaphore = xSemaphoreCreateBinary();
    xTaskCreate(senderTask, "Sender", 1000, NULL, 1, NULL);
    xTaskCreate(receiverTask, "Receiver", 1000, NULL, 1, NULL);
}

6.2. Mutexes

Giải thích chi tiết:
Mutex (Mutual Exclusion) là một dạng đặc biệt của binary semaphore, được sử dụng để bảo vệ tài nguyên chia sẻ khỏi truy cập đồng thời. Khác với semaphore thông thường, mutex có cơ chế "ownership", nghĩa là chỉ tác vụ đã lấy mutex mới có thể giải phóng nó.

Ví dụ:

SemaphoreHandle_t xMutex = NULL;

void sharedResourceTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
            // Truy cập tài nguyên chia sẻ
            Serial.println("Task accessing shared resource");
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            xSemaphoreGive(xMutex);
        }
        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

void setup() {
    Serial.begin(115200);
    xMutex = xSemaphoreCreateMutex();
    xTaskCreate(sharedResourceTask, "Task1", 1000, NULL, 1, NULL);
    xTaskCreate(sharedResourceTask, "Task2", 1000, NULL, 1, NULL);
}

6.3. Event Groups

Giải thích chi tiết:
Event Groups cho phép nhiều tác vụ đồng bộ hóa trên nhiều sự kiện. Mỗi event group chứa một số bit, mỗi bit đại diện cho một sự kiện.

Ví dụ:

#define BIT_0 (1 << 0)
#define BIT_1 (1 << 1)

EventGroupHandle_t xEventGroup;

void eventSetterTask(void *pvParameters) {
    while(1) {
        xEventGroupSetBits(xEventGroup, BIT_0);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        xEventGroupSetBits(xEventGroup, BIT_1);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void eventWaiterTask(void *pvParameters) {
    EventBits_t xBits;
    while(1) {
        xBits = xEventGroupWaitBits(xEventGroup, BIT_0 | BIT_1, pdTRUE, pdTRUE, portMAX_DELAY);
        if((xBits & (BIT_0 | BIT_1)) == (BIT_0 | BIT_1)) {
            Serial.println("Both events occurred");
        }
    }
}

void setup() {
    Serial.begin(115200);
    xEventGroup = xEventGroupCreate();
    xTaskCreate(eventSetterTask, "Setter", 1000, NULL, 1, NULL);
    xTaskCreate(eventWaiterTask, "Waiter", 1000, NULL, 1, NULL);
}

6.4. Task Notifications

Giải thích chi tiết:
Task Notifications cung cấp một cách nhẹ và nhanh để gửi tín hiệu hoặc dữ liệu giữa các tác vụ. Mỗi tác vụ có một 32-bit notification value có thể được sử dụng như một event flag hoặc để truyền dữ liệu.

Ví dụ:

void senderTask(void *pvParameters) {
    while(1) {
        vTaskDelay(1000 / portTICK_PERIOD_MS);
        xTaskNotify((TaskHandle_t)pvParameters, 0, eIncrement);
    }
}

void receiverTask(void *pvParameters) {
    uint32_t ulNotificationValue;
    while(1) {
        if(xTaskNotifyWait(0, ULONG_MAX, &ulNotificationValue, portMAX_DELAY) == pdTRUE) {
            Serial.printf("Notification received: %lu\n", ulNotificationValue);
        }
    }
}

void setup() {
    Serial.begin(115200);
    TaskHandle_t xReceiverHandle;
    xTaskCreate(receiverTask, "Receiver", 1000, NULL, 1, &xReceiverHandle);
    xTaskCreate(senderTask, "Sender", 1000, (void *)xReceiverHandle, 1, NULL);
}

Lưu ý quan trọng:

  1. Chọn cơ chế đồng bộ hóa phù hợp với yêu cầu của ứng dụng.
  2. Semaphores và mutexes có thể dẫn đến priority inversion nếu không được sử dụng cẩn thận.
  3. Task Notifications thường nhanh hơn và sử dụng ít RAM hơn so với các cơ chế khác, nhưng có hạn chế về số lượng sự kiện có thể xử lý.
  4. Khi sử dụng bất kỳ cơ chế đồng bộ hóa nào, cần cẩn thận để tránh deadlocks.

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.