1.10 Xoá Task (Deleting a Task)
1.10.1 Hàm API vTaskDelete()
Hàm API vTaskDelete() dùng để xóa một task. Hàm này chỉ có thể xài khi INCLUDE_vTaskDelete được đặt bằng 1 trong FreeRTOSConfig.h. Thực tế thì không nên tạo và xóa task liên tục trong lúc chạy, hãy cân nhắc những thiết kế khác như là xài lại các task, nếu bạn thấy phù hợp. Task đã xóa thì sẽ không tồn tại nữa và không thể vô trạng thái Running nữa.
Nếu một task đã tạo xài bộ nhớ cấp phát động (dynamic memory allocation) và sa đó cần tự xóa chính nó, thì Idle task sẽ có trách nhiệm giải phóng bộ nhớ đã cấp phát, như là stack và data structure, để xài cho việc khác. Cho nên ứng dụng không được giành thời gian xử lý của Idle task trong trường hợp này là quan trọng.
Chú ý: Khi một task bị xóa, chỉ có bộ nhớ đã cấp phát cho task bởi chính kernel được giải phóng tự động. Bất kỳ bộ nhớ hay tài nguyên nào đã được cấp phát trong lúc implement task PHẢI được free một cách tường minh nếu không cần xài nữa.
void vTaskDelete( TaskHandle_t xTaskToDelete );CCode Block 1.26. Prototype của hàm API vTaskDelete()
Các tham số của vTaskDelete()
pxTaskToDelete
Handle của task bị xóa (task chủ thể). Xem tham sốpxCreatedTaskcủa hàm APIxTaskCreate()để có thông tin về cách lấy handle của task. Một task có thể xóa chính nó bằng cách truyềnNULLthay cho task handle của nó.
Ví dụ 1.9 Xóa task
Đây là một ví dụ rất đơn giản, hành vi của nó như sau.
- Task 1 được hàm main() tạo với độ ưu tiên 1. Khi Task 1 chạy, nó tạo Task 2 với độ ưu tiên 2. Task 2 bây giờ là task có độ ưu tiên cao nhất, nên bắt đầu ngay tức thì. Code Block 1.27 là mã nguồn của main(). Code Block 1.28 là mã nguồn của Task 1.
- Task 2 không làm gì ngoài tự xóa chính nó. Nó có thể xóa chính nó bằng cách truyền NULL vô hàm API
vTaskDelete(). Nhưng vì mục đích mô phỏng, chúng ta sẽ truyền vô task handle của chính nó. Code Block 1.29 là mã nguồn của Task 2. - Khi Task 2 đã bị xóa, Task 1 lại là task có độ ưu tiên cao nhất, nên nó tiếp tục thực thi tại vị trí đã gọi
vTaskDelay()để block trong một khoảng thời gian ngắn. - Idle task thực thi trong khi Task 1 đang ở trong trạng thái Blocked và free bộ nhớ đã được cấp phát cho Task 2 mà bây giờ bị xóa.
- Khi Task 1 rời khỏi trạng thái Blocked, nó lại trở thành task có độ ưu tiên cao nhất và chiếm quyền (pre-empt) của Idle task. Khi đi vô trạng thái Running, nó sẽ tạo Task 2 một lần nữa, và mọi chuyện lặp lại tiếp diễn.
int main( void )
{
/* Create the first task at priority 1. */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
/* Start the scheduler so the task starts executing. */
vTaskStartScheduler();
/* main() should never reach here as the scheduler has been started. */
for( ;; )
{
}
}CCode Block 1.27. Hàm main() trong Ví dụ 1.9
TaskHandle_t xTask2Handle = NULL;
void vTask1( void * pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( "Task 1 is running" );
/*
* Create task 2 at a higher priority.
* Pass the address of xTask2Handle as the pxCreatedTask parameter so
* that xTaskCreate write the resulting task handle to that variable.
*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
/*
* Task 2 has/had the higher priority. For Task 1 to reach here, Task 2
* must have already executed and deleted itself.
*/
vTaskDelay( xDelay100ms );
}
}CCode Block 1.28. Hàm thực hiện Task 1 trong Ví dụ 1.9
void vTask2( void * pvParameters )
{
/*
* Task 2 immediately deletes itself upon starting.
* To do this it could call vTaskDelete() using NULL as the parameter.
* For demonstration purposes, it instead calls vTaskDelete() with its own
* task handle.
*/
vPrintLine( "Task 2 is running and about to delete itself" );
vTaskDelete( xTask2Handle );
}CCode Block 1.29. Hàm thực hiện Task 2 trong Ví dụ 1.9
C:\Temp>rtosdemo
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itselfPlaintextHình 1.16. Kết quả thực thi của Ví dụ 1.9

Hình 1.17. Lưu đồ thực thi của Ví dụ 1.9
1.11 Thread Local Storage và Reentrancy
Bộ Nhớ Cục Bộ cho Luồng (Thread Local Storage) cho phép nhà phát triển ứng dụng lưu trữ dữ liệu tùy ý (arbitrary data) trong Task Control Block của mỗi task. Tính năng này thường được xài để lưu trữ dữ liệu mà thông thường sẽ được lưu trữ trong một biến toàn cục (global variable) bởi các hàm không tái nhập (non-reentrant function).
Hàm tái nhập (reentrant function) là hàm có thể bị gián đoạn giữa chừng khi đang thực thi và sau đó được gọi lại một cách an toàn (“re-entered”) trước khi các lần gọi trước đó hoàn tất quá trình thực thi. Chúng là hàm có thể chạy an toàn từ nhiều luồng (thread) mà không gây ra tác dụng phụ nào. Khi xài hàm không tái nhập (non-reentrant function) trong môi trường đa luồng (multi-threaded environment) mà không có thread local storage, chúng ta cần phải hết sức cẩn thận kiểm tra những kết quả ngoài dự kiến của các cuộc gọi hàm này trong vùng tối quan trọng (critical section). Việc lạm dụng critical section sẽ làm giảm hiệu suất của hệ điều hành thời gian thực (RTOS), do đó người ta thường ưu tiên xài Thread Local Storage hơn là critical section.
Hầu hết thư viện libc nhúng cung cấp các API để đảm bảo rằng các hàm non-reentrant có thể làm việc đúng trong môi trường đa luồng. FreeRTOS cũng có hỗ trợ những API reentrancy là hai thư viện newlib và picolibc. Những C Runtime Thread Local Storage viết bằng C đã built sẵn này có thể được kích hoạt bằng những macro tương ứng dưới đây trong file FreeRTOSConfig.h.
configUSE_NEWLIB_REENTRANTcho thư viện newlibconfigUSE_PICOLIBC_TLScho thư viện picolibc
1.12 Scheduling Algorithms
1.12.1 Tóm Tắt Trạng Thái Của Task và Sự Kiện
Task nào mà đang thật sự chạy (đang xài thời gian xử lý) thì ở trong trạng thái Running. Trên bộ xử lý một core thì chỉ có một task có trạng thái Running trong một khoảng thời gian cho phép. Người ta cũng có thể chạy FreeRTOS trên nhiều core (asymmetric multiprocessing – AMP), hoặc để FreeRTOS lập thời khóa biểu các task trên nhiều core (symmetric multiprocessing – SMP). Không có viễn cảnh nào được mô tả ở đây.
Task không thật sự đang chạy, nhưng cũng không ở trạng thái Blocked hay Suspended, thì đang là trạng thái Ready. Các task ở trạng thái Ready thì sẵn sằng cho scheduler chọn làm task đi vô trạng thái Running. Scheduler sẽ luôn luôn chọn task Ready có độ ưu tiên lớn nhất vô trạng thái Running.
Task chờ một sự kiện (event) ở trạng thái Blocked và chúng tự động chuyển về trạng thái Ready khi sự kiện xảy ra. Sự kiện tạm thời (temporal event) xảy ra ở một thời điểm đặc biệt, lấy ví dụ như khi một khoảng thời gian block hết hạn, và thường được xài để tạo hành vi chu kỳ hoặc timeout. Sự kiện đồng bộ (synchronization event) xảy ra khi một task hoặc interrupt service routine gởi thông tin mà có xài task notification, queue, event group, message buffer, tream buffer, hoặc một trong những loại semaphore. Chúng thường được xài phổ biến để gởi tín hiệu hoạt động không đồng bộ (asynchronous activity), như là dữ liệu nhận được từ ngoại vi (peripheral).
1.12.2 Lựa chọn Thuật Toán Lập Thời Khóa Biểu
Thuật toán lập thời khóa biểu (scheduling algorithm) là chương trình con quyết định coi trạng thái task Ready nào sẽ chuyển vô trạng thái Running. Tất cả ví dụ trên đã xài cùng một scheduling algorithm. Tuy nhiên thuật toán có thể thay đổi bằng các hằng số cấu hình configUSE_PREEMPTION và configUSE_TIME_SLICING. Cả hai đều được định nghĩa trong FreeRTOSConfig.h.
Một hằng số cấu hình thứ ba là configUSE_TICKLESS_IDLE, cũng ảnh hưởng lên scheduling algorithm, do khi xài nó sẽ vô hiệu hóa (tắt) hoàn toàn tick interrupt cho những chu kỳ mở rộng (extended period). configUSE_TICKLESS_IDLE là một sự lựa chọn nâng cao được hỗ trợ đặc biệt cho những ứng dụng mà cần phải tối thiểu hóa năng lượng tiêu thụ. Những hướng dẫn trình bày ở phần này là giả sử configUSE_TICKLESS_IDLE được đặt là 1, chính là giá trị mặc định khi hằng số không cần xài.
Trong tất cả cấu hình single core, FreeRTOS scheduler lựa chọn những task có chung một độ ưu tiên theo thứ tự lần lượt. Chính sách ‘chọn theo thứ tự’ này thường gọi là ‘Round Robin Scheduling’. Thuật toán Round Robin Scheduling không đảm bảo rằng thời gian được chia đều giữa các task có cùng độ ưu tiên với nhau, mà chỉ đảm bảo những task ở trạng thái Ready và có độ ưu tiên bằng nhau sẽ lần lượt đi vô trạng thái Running theo thứ tự.
| Scheduling Algorithm | Prioritized | configUSE_PREEMPTION | configUSE_TIME_SLICING |
| Preemptive With Time Slicing | Yes | 1 | 1 |
| Preemptive Without Time Slicing | Yes | 1 | 0 |
| Co-Operative | No | 0 | Any |
Bảng 1. Cài đặt FreeRTOSConfig.h để cấu hình các kernel scheduling algorithm
1.12.3 Prioritized Preemptive Scheduling with Time Slicing
Cấu hình trong Bảng 1 cho FreeRTOS scheduler xài scheduling algorithm gọi là ‘Lập Thời Khóa Biểu Độ Ưu Tiên Cố Định có Khả Năng Chiếm Quyền với Phân Chia Thời Gian’ (Fixed Priority Preemptive Scheduling with Time Slicing) là scheduling algorithm được xài ở hầu hết ứng dụng RTOS nhỏ, và cũng là thuật toán được xài trong toàn bộ sách này. Bảng tiếp theo giải thích các thuật ngữ có trong tên của thuật toán.
Giải thích các thuật ngữ được xài để mô tả chính sách lập thời khóa biểu (scheduling policy):
- Fixed Priority
Thuật toán lập thời khóa biểu được gọi là ‘Ưu tiên cố định’ (Fixed Priority) khi nó không thay đổi độ ưu tiên gán cho các task đang được lập thời khóa biểu, nhưng vẫn cho phép các task đó tự thay đổi độ ưu tiên của chúng hoặc của các task khác. - Preemptive
Thuật toán lập thời khóa biểu ‘Chiếm quyền’ (Preemptive) sẽ ngay lập tức ‘chiếm’ (preempt) trạng thái Running của task nếu một task có độ ưu tiên cao hơn task đang ở trạng thái Running vừa vô trạng thại Ready. Bị preempt có nghĩa là một cách không tự nguyện bị di chuyển ra khỏi trạng thái Running và vô trạng thái Ready (mà không có yielding hoặc blocking một cách tường minh) để cho phép một task khác vô trạng thái Running. Sự chiếm quyền có thể xảy ra bất cứ lúc nào, không chỉ trong RTOS tick interrupt. - Time Slicing
Lát cắt thời gian (Time slicing) chia sẻ thời gian xử lý giữa các task có độ ưu tiên bằng nhau, thậm chí khi các task không yield một cách tường minh hoặc vô trạng thái Blocked. Thuật toán lập thời khóa biểu đã nói ở trên xài Time Slicing khi chọn một task mới được vô trạng thái Running ở cuối mỗi time slice nếu có những task có trạng thái Ready mà cùng độ ưu tiên như task đang chạy. Một time slice bằng với khoảng thời giữa hai lần RTOS tick interrupt.
Hình 1.18 và Hình 1.19 minh họa các task được lập thời khóa biểu như thế nào khi áp dụng thuật toán fixed priority preemptive scheduling with time slicing. Hình 1.18 vẽ lưu đồ các task được chọn vô trạng thái Running khi tất cả task trong ứng dụng có độ ưu tiên khác nhau. Hình 1.19 vẽ lưu đồ các task được chọn vô trạng thái Running khi hai task trong ứng dụng chia sẻ cùng một độ ưu tiên.

Hình 1.18. Lưu đồ thực thi mô phỏng hành vi ưu tiên của task và chiếm quyền trong một ứng dụng giả định, mà ở đó mỗi task được gán một độ ưu tiên khác nhau
Chú thích Hình 1.18:
- Idle Task
Idle task chạy ở độ ưu tiên thấp nhất, nên nó luôn bị chiếm quyền mỗi lần có task độ ưu tiên cao hơn đi vô trạng thái Ready, ví dụ như trong hình ở t3, t5 và t9. - Task 3
Task 3 là một event-driven task thực thi với độ ưu tiên tương đối thấp, như cao hơn độ ưu tiên Idle. Nó dành phần lớn thời gian của mình nằm ở trạng thái Blocked chờ đợi sự kiện (event) mà nó quan tâm, chuyển dịch từ trạng thái Blocked qua trạng thái Ready mỗi khi sự kiện xảy ra. Tất cả cơ chế giao tiếp giữa các task của FreeRTOS (task notification, queue, semaphore, event group, v.v.) đều có thể được xài để gởi tín hiệu và unblock task theo cách này.
Các sự kiện xảy ra ở thời điểm t3 và t5, và đâu đó giữa t9 và t12. Các sự kiện xảy ra ở thời điểm t3 và t5 được Task 3 xử lý ngay lập tức, bởi vì lúc đó Task 3 đang là task có độ ưu tiên cao nhất được chạy. Sự kiện xảy ra giữa t9 và t12 thì không được xử lý, do những task cao hơn là Task 1 và Task 2 vẫn đang thực thi. Chỉ đến khi t12, cả hai Task 1 và Task 2 đều ở trạng thái Blocked thì Task 3 mới lại trở thành task Ready có trạng thái cao nhất. - Task 2
Task 2 là một periodic task thực thi với độ ưu tiên trên Task 3, nhưng dưới Task 1. Khoảng thời gian của task trong hình chính là Task 2 thực thi theo chu kỳ ở thời điểm t1, t6 và t9.
Ở thời điểm t6, Task 3 đang ở trạng thái Running, nhưng Task 2 có độ ưu tiên cao hơn nên chiếm quyền của Task 3 và bắt đầu thực thi ngay. Task 2 hoàn thành xử lý của nó và quay lại trạng thái Blocked ở thời điểm t7, là lúc Task 3 có thể quay lại trạng thái Running để hoàn thành tiếp xử lý của nó. Task 3 block chính nó ở t8. - Task 1
Task 1 cũng là một event-driven task. Nó thực thi với độ ưu tiên cao nhất, nên có thể chiếm quyền của bất kỳ task nào trong hệ thống. Sự kiện của Task 1 chỉ xuất hiện ở t10, khi mà Task 1 chiếm quyền của Task 2. Task 2 có thể hoàn thành xử lý của nó chỉ sau khi Task 1 đã quay lại trạng thái Blocked ở thời điểm t11.

Hình 1.19. Lưu đồ thực thi mô phỏng hành vi ưu tiên của task và time slice trong một ứng dụng giả định, mà ở đó hai task chạy với độ ưu tiên bằng nhau
Chú thích Hình 1.19:
- Idle Task và Task 2
Idle task và Task 2 đều là continuous processing task và cùng có độ ưu tiên 0 (độ ưu tiên thấp nhất). Scheduler chỉ cấp thời gian xử lý cho các task có độ ưu tiên 0 khi không có task nào có độ ưu tiên cao hơn đang chạy, và chia thời gian cho các task có độ ưu tiên 0 bằng time slice. Một time slice mới bắt đầu ở mỗi tick interrupt, trong Hình 1.19 là xảy ra ở t1, t2, t3, t4, t5, t8, t9, t10 và t11.
Idle task và Task 2 lần lượt vô trạng thái Running khiến cho cả hai đồng thời ở trạng thái Running ở mỗi phần trong cùng time slice, xảy ra ở khoảng thời gian giữa t5 và t8. - Task 1
Độ ưu tiên của Task 1 cao hơn của Idle task. Task 1 là một event-driven task dành hầu hết thời gian của nó ở trạng thái Blocked chờ đợi sự kiện mà nó quan tâm, chuyển từ trạng thái Blocked thành trạng thái Ready mỗi khi sự kiện xảy ra.
Sự kiện xảy ra ở thời điểm t6. Ở t6, Task 1 trở thành task có độ ưu tiên cao nhất được chạy, và do đó Task 1 chiếm quyền của Idle task trong một phần của time slice. Xử lý sự kiện kết thúc ở t7, Task 1 quay lại trạng thái Blocked.
Hình 1.19 cho thấy Idle task chia sẻ thời gian với một task của ứng dụng. Cấp nhiều thời gian xử lý cho Idle task như vậy có thể không hợp lý, nếu task do người viết ứng dụng tạo có độ ưu tiên bằng Idle task có việc cần làm, còn Idle task thì không làm gì hết. Chúng ta có thể xài hằng số cấu hình configIDLE_SHOULD_YIELD để thay đổi cách Idle task được xếp thời khóa biểu:
- Khi
configIDLE_SHOULD_YIELDđặt là 0 thì Idle task sẽ duy trì ở trạng thái Running trong toàn khoảng time slice của nó, tới khi nó bị task cao hơn chiếm quyền. - Khi
configIDLE_SHOULD_YIELDđặt là 1 thì Idle task sẽ yield (tự nguyện bỏ phần time slice còn lại) ở mỗi vòng lặp của nó nếu có task khác bằng độ ưu tiên của Idle đang ở trạng thái Ready.
Lưu đồ thực thi trong Hình 1.19 là quan sát khi configIDLE_SHOULD_YIELD đặt là 0. Lưu đồ thực thi trong Hình 1.20 là quan sát khi configIDLE_SHOULD_YIELD đặt là 1.

Hình 1.20. Lưu đồ thực thi cho viễn cảnh tương tự Hình 1.19, nhưng lần này configIDLE_SHOULD_YIELD đặt là 1
Hình 1.20 cũng cho thấy khi configIDLE_SHOULD_YIELD được đặt là 1, Idle task thực thi và yield để bỏ phần time slice còn lại cho Task 2 được chọn vô trạng thái Running trong toàn bộ time slice.
1.12.4 Prioritized Preemptive Scheduling without Time Slicing
Xếp Thời Khóa Biểu Ưu Tiên Chiếm Quyền Không Có Chia Lát Cắt Thời Gian (Prioritized Preemptive Scheduling without time slicing) bảo trì các thuật toán chọn task và chiếm quyền như ở phần trước, nhưng không xài time slicing để chia sẻ thời gian xử lý giữa các task có cùng độ ưu tiên.
Bảng 1 cho thấy các cài đặt trong FreeRTOSConfig.h dùng để cấu hình FreeRTOS scheduler xài thuật toán prioritized preemptive scheduling without time slicing.
Như đã minh họa trong Hình 1.19, nếu có xài time slicing, và có nhiều task ở trạng thái Ready mà có độ ưu tiên cao nhất có thể chạy, thì scheduler sẽ chọn một task mới được vô trạng thái Running ở mỗi RTOS tick interrupt (tick interrupt đánh dấu kết thúc của một time slice). Nếu không xài time slicing, thì scheduler chỉ chọn một task mới để đưa vô trạng thái Running khi xảy ra một trong các trường hợp sau:
- Một task có độ ưu tiên cao hơn vô trạng thái Ready.
- Task ở trạng thái Running vô trạng thái Blocked hoặc Suspended.
Khi không xài time slicing thì có ít lần chuyển đổi ngữ cảnh task (task context switch) hơn so với khi có xài time slicing. Do đó, tắt time slicing sẽ cắt giảm tổng chi phí xử lý của scheduler. Tuy nhiên, việc tắt time slicing cũng có thể khiến các task có cùng độ ưu tiên nhận được lượng thời gian xử lý rất khác nhau, một tình huống được minh họa trong Hình 1.21. Vì lý do này, chạy scheduler mà không xài time slicing được xem là một kỹ thuật nâng cao, chỉ nên được xài bởi những người dùng có kinh nghiệm.

Hình 1.21. Lưu đồ thực thi mô tả các task có độ ưu tiên bằng nhau có thể nhận được lượng thời gian xử lý rất khác nhau khi không xài time slicing
Nhìn Hình 1.21, giả sử configIDLE_SHOULD_YIELD được đặt là 0:
- Tick interrupt
Tick interrupt xảy ra lúc t1, t2, t3, t4, t5, t8, t11, t12 và t13. - Task 1
Task 1 là một event-driven task có độ ưu tiên dành hầu hết thời gian của nó ở trạng thái Blocked chờ đợi sự kiện mà nó quan tâm. Task 1 chuyển từ trạng thái Blocked thành trạng thái Ready mỗi khi có sự kiện xảy ra và cũng bởi vì nó là task có độ ưu tiên cao nhất ở trạng thái Ready nên được vô trạng thái Running. Hình 1.21 cho thấy Task 1 xử lý một sự kiện giữa t6 và t7, và một lần nữa giữa t9 và t10. - Idle task và Task 2
Idle task và Task 2 đều là continuous processing task, và cả hai đều có độ ưu tiên bằng 0 (idle priority). Lưu ý là continuous processing task thì không bước vô trạng thái Blocked.
Time slicing không được xài, nên một Idle priority task đang ở trạng thái Running sẽ duy trì ở trạng thái Running cho đến khi bị chiếm quyền bởi Task 1 có độ ưu tiên cao hơn.
Trong Hình 1.21, Idle task bắt đầu chạy ở thời điểm t1, và vẫn ở trạng thái Running cho đến khi nó bị chiếm quyền bởi Task 1 ở thời điểm t6, tức là hơn bốn tick period sau khi nó bước vô trạng thái Running.
Task 2 bắt đầu chạy ở thời điểm t7, khi Task 1 quay lại trạng thái Blocked để chờ một sự kiện khác. Task 2 vẫn ở trạng thái Running cho đến khi nó lại bị chiếm quyền bởi Task 1 ở thời điểm t9, tức là ít hơn một tick period sau khi nó vô trạng thái Running.
Ở thời điểm t10, Idle task lại quay lại trạng thái Running, mặc dù nó đã nhận được lượng thời gian xử lý nhiều hơn gấp bốn lần so với Task 2.
1.12.5 Cooperative Scheduling
Cuốn sách này tập trung vào preemptive scheduling, nhưng FreeRTOS cũng có thể xài cooperative scheduling. Bảng 1 cho thấy các cài đặt trong FreeRTOSConfig.h dùng để cấu hình FreeRTOS scheduler xài cooperative scheduling.
Khi xài cooperative scheduler (và giả sử rằng các interrupt service routine của ứng dụng không yêu cầu chuyển ngữ cảnh một cách tường minh) một chuyển ngữ cảnh (context switch) chỉ xảy ra khi task ở trạng thái Running bước vô trạng thái Blocked, hoặc task ở trạng thái Running chủ động yield (tự yêu cầu sắp xếp lại thời khóa biểu) bằng cách gọi taskYIELD(). Các task sẽ không bao giờ bị chiếm quyền, cho nên time slicing không thể được xài.
Hình 1.22 minh họa hành vi của cooperative scheduler. Các đường gạch ngang nằm ngang trong Hình 1.22 cho thấy khi một task đang ở trạng thái Ready.

Hình 1.22. Lưu đồ thực thi minh họa hành vi của cooperative scheduler
- Task 1
Task 1 có độ ưu tiên cao nhất. Nó bắt đầu ở trạng thái Blocked, chờ một semaphore.
Ở thời điểm t3, một interrupt gởi (give) semaphore, khiến Task 1 rời trạng thái Blocked và bước vô trạng thái Ready (việc gởi semaphore từ interrupt sẽ được trình bày trong một chương sau).
Ở thời điểm t3, Task 1 là task ở trạng thái Ready có độ ưu tiên cao nhất, và nếu preemptive scheduler được xài thì Task 1 sẽ trở thành task ở trạng thái Running. Tuy nhiên, vì cooperative scheduler đang được xài, Task 1 vẫn ở trạng thái Ready cho đến thời điểm t4, khi task đang ở trạng thái Running gọitaskYIELD(). - Task 2
Độ ưu tiên của Task 2 nằm giữa Task 1 và Task 3. Nó bắt đầu ở trạng thái Blocked, chờ một tin nhắn (message) được gởi (send) từ Task 3 ở thời điểm t2.
Ở thời điểm t2, Task 2 là task ở trạng thái Ready có độ ưu tiên cao nhất, và nếu preemptive scheduler được xài thì Task 2 sẽ trở thành task ở trạng thái Running. Tuy nhiên, vì cooperative scheduler đang được xài, Task 2 vẫn ở trạng thái Ready cho đến khi task đang ở trạng thái Running bước vô trạng thái Blocked hoặc gọitaskYIELD().
Task đang ở trạng thái Running gọitaskYIELD()ở thời điểm t4, nhưng lúc đó Task 1 là task ở trạng thái Ready có độ ưu tiên cao nhất, nên Task 2 thực tế không trở thành task ở trạng thái Running cho đến khi Task 1 quay lại trạng thái Blocked ở thời điểm t5.
Ở thời điểm t6, Task 2 quay lại trạng thái Blocked để chờ message tiếp theo, lúc này Task 3 lại một lần nữa trở thành task ở trạng thái Ready có độ ưu tiên cao nhất.
Trong một ứng dụng multi-task, người viết ứng dụng phải chú ý để một tài nguyên (resource) không bị truy cập bởi nhiều task cùng lúc, vì việc truy cập đồng thời có thể làm hỏng resource. Ví dụ, hãy xét tình huống sau, trong đó resource được truy cập là một UART (serial port). Hai task ghi chuỗi vô UART; Task 1 ghi “abcdefghijklmnop”, và Task 2 ghi “123456789”:
- Task 1 đang ở trạng thái Running và bắt đầu ghi chuỗi của nó. Nó ghi “abcdefg” vô UART, nhưng rời trạng thái Running trước khi ghi thêm các ký tự khác.
- Task 2 bước vô trạng thái Running và ghi “123456789” vô UART, rồi rời trạng thái Running.
- Task 1 quay lại trạng thái Running và ghi các ký tự còn lại của chuỗi của nó vô UART.
Trong tình huống đó, chuỗi thực sự được ghi vô UART là “abcdefg123456789hijklmnop”. Chuỗi mà Task 1 ghi đã không được vô UART theo một trình tự liên tục như mong muốn, mà đã bị hỏng, do chuỗi mà Task 2 ghi vô UART xuất hiện chung với nó.
Xài cooperative scheduler thường giúp tránh các vấn đề gây ra do truy cập đồng thời hơn, so với khi xài preemptive scheduler:
- Khi xài preemptive scheduler, task ở trạng thái Running có thể bị preempted bất cứ lúc nào, kể cả khi resource mà nó đang chia sẻ với task khác đang ở một trạng thái không nhất quán (inconsistent state). Như ví dụ UART vừa nói đến, việc để resource ở trạng thái không nhất quán có thể làm hư dữ liệu.
- Khi xài cooperative scheduler, bạn kiểm soát được khi nào việc chuyển sang task khác xảy ra. Do đó, bạn có thể đảm bảo rằng việc chuyển sang task khác không xảy ra trong lúc resource đang ở trạng thái không nhất quán.
- Trong ví dụ UART ở trên, bạn có thể đảm bảo Task 1 không rời trạng thái Running cho đến khi ghi xong toàn bộ chuỗi của nó vô UART, và bằng cách đó loại bỏ khả năng chuỗi bị hư bởi hoạt động của task khác.
(Các phương pháp chia sẻ resource một cách an toàn (safely sharing resources) giữa các task sẽ được nói kỹ hơn trong cuốn sách này. Những resource do chính FreeRTOS cung cấp, như queue và semaphore, thì luôn an toàn để chia sẻ giữa các task).
Như minh họa ở Hình 1.22, xài cooperative scheduler làm cho hệ thống phản ứng chậm hơn so với khi xài preemptive scheduler:
- Khi xài preemptive scheduler, scheduler sẽ bắt đầu chạy một task ngay khi task đó trở thành task Ready có độ ưu tiên cao nhất. Điều này rất cần thiết trong các hệ thống thời gian thực (real-time) vốn phải phản hồi những sự kiện có độ ưu tiên cao trong một khoảng thời gian xác định.
- Khi xài cooperative scheduler, việc chuyển sang một task vừa trở thành task Ready có độ ưu tiên cao nhất sẽ không được thực hiện cho đến khi task ở trạng thái Running bước vô trạng thái Blocked hoặc gọi
taskYIELD().