Cooperative Task Scheduling in Go
Cooperative scheduling allows tasks to voluntarily yield control to other tasks, creating a form of concurrency without the complexities of preemptive scheduling. This challenge asks you to implement a simple cooperative scheduler in Go, enabling multiple tasks to run in a controlled and predictable manner. This is useful for scenarios where you want fine-grained control over task execution order and avoid the overhead of OS-level threads.
Problem Description
You are to implement a cooperative scheduler that manages a set of tasks. Each task is a function that performs some work and then explicitly yields control back to the scheduler. The scheduler should maintain a queue of tasks and execute them one at a time until all tasks have completed. The scheduler must ensure that tasks are executed in the order they are added to the queue.
Key Requirements:
- Task Definition: A task should be a function with no arguments and no return values (
func()). - Scheduler: The scheduler should be a struct containing a queue of tasks (a slice of
func()). - Add Task: A method to add a task to the scheduler's queue.
- Run: A method to execute all tasks in the queue, one after another, until the queue is empty. Each task must yield control back to the scheduler after its execution.
- Yield: A function that allows a task to voluntarily yield control back to the scheduler. This is the core of cooperative scheduling.
Expected Behavior:
The Run method should iterate through the tasks in the order they were added to the queue. Each task should be executed completely before the next task is started. The Yield function should effectively pause the current task and allow the scheduler to pick the next task from the queue.
Edge Cases to Consider:
- Empty task queue: The
Runmethod should return gracefully without panicking. - Tasks that don't yield: While the problem description assumes tasks will yield, consider how your scheduler might handle a task that fails to yield (though this is not a primary requirement for this challenge). A simple approach is to let the program hang.
- Adding tasks while the scheduler is running: This is outside the scope of this challenge.
Examples
Example 1:
Input:
tasks := Scheduler{[]func(){
func() { print("Task 1") } ,
func() { print("Task 2") } ,
func() { print("Task 3") } ,
}}
Output:
Task 1
Task 2
Task 3
Explanation:
The tasks are added to the scheduler in the order Task 1, Task 2, Task 3. The scheduler executes them in that same order, printing each task's output to the console.
Example 2:
Input:
tasks := Scheduler{[]func(){
func() { print("A") ; Yield() ; print("A2") },
func() { print("B") },
}}
Output:
A
B
A2
Explanation:
Task A prints "A", then yields. Task B prints "B". Then, Task A resumes and prints "A2".
Example 3: (Edge Case)
Input:
tasks := Scheduler{[]func(){}} // Empty task queue
Output:
(No output)
Explanation:
The scheduler's Run method handles the empty queue gracefully and returns without executing any tasks.
Constraints
- The scheduler should be implemented using Go's built-in concurrency primitives (slices, functions). No external libraries are allowed.
- The
Yieldfunction should be implemented using a simple mechanism that effectively pauses the current goroutine. Atime.Sleep(0)call is a common and acceptable approach. - The time taken to execute each task should be relatively short (less than 10ms) to avoid excessive delays.
- The order of task execution must be strictly FIFO (First-In, First-Out).
Notes
- The
Yieldfunction is crucial for cooperative scheduling. It allows tasks to voluntarily relinquish control, enabling the scheduler to switch to another task. - Consider how you will structure your code to make it modular and easy to understand.
- Think about how to handle the case where a task doesn't yield (although a robust solution to this is not required). A simple hang is acceptable.
- This is a simplified cooperative scheduler. Real-world cooperative schedulers often have more sophisticated features, such as priorities and timeouts.