Mutex (Mutually exclusive) ใช้ป้องกันการเข้าใช้ทรัพยากรเดียวกันในเวลาเดียวกันจากหลาย Task โดยล็อกโค้ดส่วนที่ต้องการป้องกันด้วยคำสั่ง Take และ Give
แนวคิดของ FreeRTOS มองว่า Mutex คือการสร้างกุญแจขึ้นมา 1 ดอก เมื่อ Task ใด ๆ ต้องการเข้าถึงทรัพยากร จะต้องนำกุญแจออกไป เมื่อทำงานเสร็จจึงนำกุญแจมาคืน โดยการนำกุญแจออกไปจะเรียกว่าการ Take ส่วนการคืนกุญแจจะเรียกว่าการ Give โดยทุกครั้งที่มีการ Take เมื่อโค้ดโปรแกรมส่วนนั้นทำงานเสร็จจะต้องมีการ Give เสมอ เพื่อเปิดโอกาศให้ Task อื่น ๆ ได้เข้ามาใช้ทรัพยากรต่อไป
การ Take, Give ในบางแหล่งข้อมูลจะใช้คำว่า Lock และ Unlock แทน ตัวอย่างรูปด้านแสดงการใช้ Mutex เพื่อล็อกการใช้งานทรัพพยากรกลาง (Mutex) จาก Task A (Thread A) และ Task B (Thread B)
ตัวอย่างแนวคิดเรื่องกุญแจแสดงด้านล่าง โดยเว็บไซต์ต้นทางได้อธิบายรูปภาพไว้ดังนี้
มีห้องน้ำอยู่ 1 ห้อง การเข้าห้องน้ำต้องใช้กุญแจ แต่มี 3 คนรอใช้ห้องน้ำอยู่ การเข้าห้องน้ำจะเข้าได้ครั้งละ 1 คนเท่านั้น เมื่อคนแรกออกมาจากห้องน้ำแล้ว จึงจะส่งต่อกุญแจให้คนถัดไป
บทความนี้ใช้ ESP32 กับแพลตฟอร์ม Arduino ในการทดสอบ
ทดสอบโค้ดโปรแกรมทำงานซ้อนกันเพราะไม่ใช้ Mutex
ตัวอย่างโค้ดโปรแกรมด้านล่างนี้สร้าง Task A, Task B ขึ้นมา แล้วเรียกใช้ฟังก์ชั่น print_number(); จาก Task A, Task B แต่ใน Task B มีการหน่วงเวลาด้วยคำสั่ง delay(); ให้เกิดการ Overlab ของเวลาเล็กน้อย
#include <Arduino.h>
void print_number() { // ฟังก์ชั่นให้ print 0 1 ... 9 ออกทาง Serial Monitor
for (int i=0;i<10;i++) {
Serial.printf("%d\t", i);
delay(10);
}
Serial.println();
}
void task_a(void*) { // สร้างฟังก์ชั่น Task A
print_number(); // เรียกใช้ฟังก์ชั่นแสดงเลข 0 1 2 ... 9
vTaskDelete(NULL);
}
void task_b(void*) { // สร้างฟังก์ชั่น Task B
delay(30); // หน่วงเวลา 30 วินาที เพื่อให้เกิดการ Overlab
print_number(); // เรียกใช้ฟังก์ชั่นแสดงเลข 0 1 2 ... 9
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
xTaskCreate(task_a, "task_a", 2 * 1024, NULL, 20, NULL); // สร้าง Task A
xTaskCreate(task_b, "task_b", 2 * 1024, NULL, 20, NULL); // สร้าง Task B
}
void loop() {
delay(500);
}
ความคาดหวังคือตัวเลขควรแสดงออกมา 0 1 2 3 .... 9 ไปเรื่อย ๆ แต่ผลที่ได้คือ 0 1 2 0 3 1 4 ... 9 7 8 9 จะเห็นว่าตัวเลขที่แสดงออกมาไม่เรียงกัน เพราะเลข 0 1 2 ชุดแรกเกิดจาก Task A แต่เลขที่แทรกมา เกิดจาก Task B
ปรับโค้ดโปรแกรมใหม่โดยใส่ Mutex เข้าไปล็อกการทำงาน ได้โค้ดโปรแกรมใหม่ ดังนี้
#include <Arduino.h>
QueueHandle_t xPrintNumberMutex; // สร้างตัวแปรเก็บ Handle ของ Mutex
void print_number() { // ฟังก์ชั่นให้ print 0 1 ... 9 ออกทาง Serial Monitor
if (xSemaphoreTake(xPrintNumberMutex, pdMS_TO_TICKS(1000)) == pdFALSE) { // Take Mutex
Serial.println("Take mutex fail"); // แสดงข้อความ "Take mutex fail" หาก Take ไม่สำเร็จ
return;
}
for (int i=0;i<10;i++) {
Serial.printf("%d\t", i);
delay(10);
}
Serial.println();
xSemaphoreGive(xPrintNumberMutex); // Give Mutex
}
void task_a(void*) { // สร้างฟังก์ชั่น Task A
print_number(); // เรียกใช้ฟังก์ชั่นแสดงเลข 0 1 2 ... 9
vTaskDelete(NULL);
}
void task_b(void*) { // สร้างฟังก์ชั่น Task B
delay(30); // หน่วงเวลา 30 วินาที เพื่อให้เกิดการ Overlab
print_number(); // เรียกใช้ฟังก์ชั่นแสดงเลข 0 1 2 ... 9
vTaskDelete(NULL);
}
void setup() {
Serial.begin(115200);
xPrintNumberMutex = xSemaphoreCreateMutex(); // สร้าง Mutex เก็บ Handle ลงตัวแปร xPrintNumberMutex
xTaskCreate(task_a, "task_a", 2 * 1024, NULL, 20, NULL); // สร้าง Task A
xTaskCreate(task_b, "task_b", 2 * 1024, NULL, 20, NULL); // สร้าง Task B
}
void loop() {
delay(500);
}
ผลที่ได้เป็นไปตามที่คาดหวัง คือแสดงเลข 0 ... 9 เรียงลำดับถูกต้อง จำนวน 2 ชุด โดยชุดแรกมาจาก Task A และชุดที่ 2 มาจาก Task B
การสร้าง Mutex
ใช้คำสั่ง xSemaphoreCreateMutex() สร้าง Mutex โดยใช้แรมที่เหลือ ณ ขณะนั้น หรือใช้คำสั่ง xSemaphoreCreateMutexStatic() สร้างโดยใช้แรมที่ขอไว้ล่วงหน้า
SemaphoreHandle_t xSemaphoreCreateMutex();
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer);
พารามิเตอร์: ไม่มี
ค่าที่ส่งกลับ: ข้อมูลชนิด SemaphoreHandle_t โดย
หากสร้าง Mutex สำเร็จ - ส่งกลับ handle ของ Mutex นี้ จำเป็นต้องใช้ในคำสั่ง Take หรือ Give ต่อไป
หากไม่สำเร็จ (แรม ณ ขณะนั้นเหลือไม่พอให้สร้าง Mutex) - ส่งกลับ NULL
ตัวอย่างการสร้าง Mutex มีดังนี้
#include <Arduino.h>
QueueHandle_t xPrintNumberMutex; // สร้างตัวแปรเก็บ Handle ของ Mutex
void setup() {
xPrintNumberMutex = xSemaphoreCreateMutex(); // สร้าง Mutex
}
การ Take Mutex
ใช้คำสั่ง xSemaphoreTake() กรณี Take Mutex นอกฟังก์ชั่นอินเตอร์รัพท์ หรือ xSemaphoreTakeFromISR() กรณีต้องการ Take Mutex จากในฟังก์ชั่นอินเตอร์รัพท์
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken)
พารามิเตอร์:
(SemaphoreHandle_t) xSemaphore - handle ของ Mutex ที่ต้องการจะ Take ค่า
(TickType_t) xTicksToWait - ระยะเวลานานที่สุดที่จะรอ Mutex ถูก Give ในหน่วย Tick
ค่าที่ส่งกลับ: (BaseType_t) สถานะของการ Take Mutex โดย
pdTRUE - สามารถ Task Mutex ได้สำเร็จ ให้เข้าใช้งานโค้ดโปรแกรมในบรรทัดต่อไปได้
pdFALSE - ไม่สามารถ Task Mutex ได้ภายในเวลาที่กำหนด (Timeout) ต้องห้ามเข้าใช้โค้ดที่ล็อกไว้
ตัวอย่างการ Task Mutex มีดังนี้
if (xSemaphoreTake(xPrintNumberMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { // Take Mutex
...; // โค้ดโปรแกรมส่วนที่ต้องการล็อกการทำงาน
xSemaphoreGive(xPrintNumberMutex); // Give Mutex
}
การ Give Mutex
ใช้คำสั่ง xSemaphoreGive() กรณี Give Mutex นอกฟังก์ชั่นอินเตอร์รัพท์ หรือ xSemaphoreGiveFromISR() กรณีต้องการ Give Mutex จากในฟังก์ชั่นอินเตอร์รัพท์
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken) ;
พารามิเตอร์:
(SemaphoreHandle_t) xSemaphore - handle ของ Mutex ที่ต้องการจะ Give ค่า
ค่าที่ส่งกลับ: (BaseType_t) สถานะของการ Take Mutex โดย
pdTRUE - สามารถ Give Mutex ได้สำเร็จ
errQUEUE_FULL - ไม่สามารถ Give Mutex ได้
ตัวอย่างการ Give Mutex มีดังนี้
if (xSemaphoreTake(xPrintNumberMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { // Take Mutex
...; // โค้ดโปรแกรมส่วนที่ต้องการล็อกการทำงาน
xSemaphoreGive(xPrintNumberMutex); // Give Mutex
}
ข้อมูลเพิ่มเติม
Semaphores API : https://www.freertos.org/a00113.html
FreeRTOS Mutexes : https://www.freertos.org/Real-time-embedded-RTOS-mutexes.html
Comments