Một task (hay còn gọi là thread) là một luồng thực thi độc lập. Mỗi thread phải cạnh tranh với các thread khác để giành thời gian xử lý của bộ vi xử lý. Trong một hệ thống chỉ có một bộ xử lý (single processor), tính đồng thời (concurrency) chỉ là một “ảo giác”.
Khi chúng ta dùng nhiều thread, cảm giác như các thread chạy cùng lúc. Nó xuất hiện vì context switching giữa các thread diễn ra rất nhanh. Tuy vậy, việc chuyển đổi này không miễn phí. Mỗi lần chuyển, hệ thống phải lưu lại và khôi phục trạng thái thực thi của thread. Việc này tạo ra overhead cho context switching.
1. Context Switching
Cách hệ thống chuyển đổi giữa các thread phụ thuộc vào scheduling algorithm mà nó dùng. Zephyr RTOS cho phép chúng ta cấu hình nhiều loại scheduling strategy khác nhau để quản lý việc chuyển đổi này.
Trong hệ thống đa nhiệm (multitasking), chúng ta các cách chính để điều khiển luồng (thread) như sau:
-
Đầu tiên là Thread priority-based preemptive multitasking. Ở cách này, nếu một luồng có mức ưu tiên thấp đang chạy mà một luồng khác có mức ưu tiên cao hơn đủ điều kiện chạy, hệ thống sẽ dừng luồng ưu tiên thấp lại. Lúc này, luồng ưu tiên cao sẽ chạy ngay lập tức.
-
Cách thứ hai là Cooperative multitasking. Ở đây, từng luồng tự quyết định khi nào nên tạm dừng để các luồng khác có cơ hội chạy. Hệ thống chỉ chuyển sang luồng khác khi luồng hiện tại chủ động nhường quyền.
-
Cách thứ ba là Round-robin scheduling là một phương pháp quản lý tài nguyên trong máy tính. Ở đây, mỗi thread sẽ được cấp một khoảng thời gian ngắn để chạy. Sau đó, hệ thống chuyển sang thread tiếp theo. Quá trình này lặp lại theo thứ tự. Nhờ vậy, tất cả thread đều có cơ hội xử lý công việc.
Ngoài Round-robin, còn có nhiều cách sắp xếp khác. Những phương pháp này có thể ưu tiên cho một số thread quan trọng hơn. Chúng ta chọn cách phù hợp tùy vào mục tiêu của chương trình.
2. Cách một task hoạt động
Khi chúng ta tạo một thread mới, hệ thống sẽ cấp cho nó một bộ thông tin riêng. Mỗi thread đều có tên (Name) để phân biệt. Nó nhận một mã số duy nhất (Unique ID) để hệ thống dễ quản lý. Nếu dùng cơ chế lập lịch ưu tiên (preemptive scheduler), thread còn có thêm thuộc tính Priority để xác định mức độ ưu tiên khi chạy. Thread cũng có một vùng lưu trữ thông tin trạng thái, gọi là task control block. Ngoài ra, thread sẽ có một vùng nhớ riêng (stack) để lưu trữ dữ liệu tạm thời khi thực thi. Cuối cùng, thread gắn với một hàm nhiệm vụ (task routine) để thực hiện công việc cụ thể.
Khi một real-time kernel khởi động, nó sẽ tạo ra các system tasks và user tasks. Mỗi task này có priority và privilege riêng. Các system tasks thường gặp bao gồm: task khởi động kernel, idle task dùng CPU khi không có việc khác, task ghi log để lưu lại thông điệp hệ thống, task xử lý exception khi phát hiện lỗi, và task debug agent khi chúng ta cần gỡ lỗi chương trình. Nếu hệ thống cần tiết kiệm năng lượng, idle task sẽ đưa hệ thống vào chế độ tiết kiệm điện hoặc sleep mode. Các chi tiết cụ thể có thể khác nhau tùy vào từng RTOS.
Trong một hệ thống multitasking, mỗi task luôn ở một trong vài trạng thái xác định. Trạng thái quan trọng nhất là ready-to-run, tức là task sẵn sàng để chạy. Nếu task cần chờ tài nguyên, sự kiện hoặc hết thời gian sleep, nó sẽ vào trạng thái waiting hoặc blocked. Khi task thực sự chạy trên CPU, nó ở trạng thái running. Ngoài ra, một số hệ điều hành thời gian thực (RTOS) còn cho phép chúng ta suspend (tạm dừng) hoặc terminate (kết thúc) một task.
Hành vi của một task trong hệ thống preemptive multitasking có thể mô tả bằng Finite State Machine (FSM). FSM giúp chúng ta hiểu các trạng thái khác nhau của task.

Một task có thể tạm dừng vì nhiều lý do. Ví dụ, task có thể phải chờ một synchronization object như mutex hoặc semaphore được giải phóng. Đôi khi, task cũng phải đợi message đến trong message queue. Ngoài ra, task còn có thể chờ cho đến khi hết thời gian delay đã đặt trước.
Khi một task không còn bị block, task đó sẽ chạy nếu nó có priority cao nhất trong số các task đủ điều kiện chạy. Nếu không, task sẽ được đặt vào đúng vị trí trong hàng đợi các task ready-to-run. Trong Zephyr, trạng thái Running chỉ áp dụng cho các thread ở trạng thái Ready. Chúng ta cần nhớ rằng tại một thời điểm, chỉ có một thread thực sự chạy. Kernel chịu trách nhiệm tạo và quản lý các thread. Kernel cũng cung cấp các phương thức để tạo, xóa, điều khiển lịch chạy của các task, và lấy thông tin về các task.
Quản lý task và các resource liên quan là phần rất quan trọng khi chúng ta xây dựng ứng dụng real-time. Khi một task kết thúc, bạn cần đảm bảo đã giải phóng đúng các resource mà task đó sử dụng. Nếu hệ thống có ít resource và không có memory management unit, việc tạo và xóa task liên tục sẽ gây rắc rối. Điều này có thể làm cho hệ thống khó giữ được tính deterministic, tức là khó dự đoán chính xác kết quả. Ngoài ra, tạo và xóa task liên tục còn tăng nguy cơ memory fragmentation, khiến bộ nhớ bị chia nhỏ và khó sử dụng hiệu quả.
Việc tạo task động chỉ hợp lý khi hệ thống đóng vai trò như một server phục vụ nhiều client khác nhau. Tuy nhiên, với các hệ thống nhúng bị giới hạn về tài nguyên, chúng ta nên thiết kế số lượng task, công việc và priority của từng task ngay từ đầu. Cách này giúp ứng dụng dễ kiểm thử và giữ được tính deterministic cao.
Nhiều nhân hệ điều hành thời gian thực (RTOS kernels), như Zephyr, cung cấp một API để chúng ta kiểm soát cách lập lịch các task. API này cho phép bạn tạm dừng (suspend), tiếp tục (resume), trì hoãn (delay), và khởi động lại (restart) task. Ngoài ra, bạn cũng có thể lấy và thay đổi mức ưu tiên (priority) của task.
Các API của RTOS còn cung cấp phương thức để bạn xem chi tiết về các task. Ví dụ, bạn có thể lấy ID của các task đã được cài đặt, thông tin về task control block, và xem các tài nguyên mà mỗi task đang sở hữu.
Trong một ứng dụng, task có thể là “run-to-completion” hoặc “infinite loop”. Thường thì, một task “run-to-completion” có mức ưu tiên cao. Nó sẽ khởi tạo và bắt đầu các task khác, sau đó tự kết thúc. Hầu hết các task trong ứng dụng thường là “infinite loop”, nghĩa là chúng lặp đi lặp lại mãi cho đến khi hệ thống dừng.