Hone logo
Hone
Problems

Preemptive Task Scheduler in Go

This challenge asks you to build a preemptive task scheduler in Go. A preemptive scheduler allows a running task to be interrupted and resumed later, enabling higher-priority tasks to execute immediately. This is crucial for applications requiring responsiveness, like operating systems or real-time systems, where immediate action is sometimes needed.

Problem Description

You need to implement a Scheduler type in Go that manages a collection of Task objects. The scheduler should be able to:

  1. Add tasks: Tasks can be added to the scheduler with a priority. Higher numerical priority values should indicate higher priority.
  2. Execute tasks: The scheduler should run tasks in a preemptive manner. When a new task with a higher priority than the currently running task is added, the current task should be paused, and the new higher-priority task should start executing.
  3. Task execution model: Tasks will be represented by a Task struct that has an ID, a Priority, and a Run method. The Run method should simulate work by printing messages and potentially yielding control.
  4. Task preemption: If a task is running and a new task with a strictly higher priority is added, the running task must be paused. The paused task should retain its current execution state so it can be resumed later.
  5. Task completion: When a task finishes executing, the scheduler should resume the highest-priority waiting task.
  6. Concurrency: The scheduler should manage concurrency safely, likely using Go's concurrency primitives.

Key Requirements:

  • A Scheduler struct capable of holding and managing tasks.
  • A Task struct with ID, Priority, and a Run method.
  • The Scheduler must have an AddTask(task Task) method.
  • The Scheduler must have a Run() method to start the scheduling loop.
  • The Run method of a Task should be able to be started, paused, and resumed. This can be simulated by using channels or other synchronization mechanisms within the Task.Run method.

Expected Behavior:

The scheduler should always be executing the highest-priority task that is ready to run. If multiple tasks have the same highest priority, their execution order can be determined by their arrival order (FIFO within priority).

Important Edge Cases:

  • Adding a task with the same priority as the currently running task.
  • Adding a task with a lower priority than the currently running task.
  • Adding multiple high-priority tasks rapidly.
  • Tasks that take a long time to complete.
  • An empty scheduler.

Examples

Example 1:

Input:
Tasks to add:
- Task A (ID: 1, Priority: 2)
- Task B (ID: 2, Priority: 1)
- Task C (ID: 3, Priority: 3)

Scheduler starts. Task B (priority 1) is added.
Task B starts running.

Task A (priority 2) is added. Priority of A > Priority of B.
Task B is paused. Task A starts running.

Task C (priority 3) is added. Priority of C > Priority of A.
Task A is paused. Task C starts running.

Task C finishes.
Scheduler resumes Task A (highest remaining priority).

Task A finishes.
Scheduler resumes Task B (highest remaining priority).

Task B finishes.

Output:
Task 2 (Priority 1) started.
Task 2 (Priority 1) running...
Task 1 (Priority 2) added. Preempting Task 2.
Task 1 (Priority 2) started.
Task 3 (Priority 3) added. Preempting Task 1.
Task 3 (Priority 3) started.
Task 3 (Priority 3) finished.
Task 1 (Priority 2) resumed.
Task 1 (Priority 2) finished.
Task 2 (Priority 1) resumed.
Task 2 (Priority 1) finished.

Example 2:

Input:
Tasks to add:
- Task X (ID: 10, Priority: 5)
- Task Y (ID: 11, Priority: 5)

Scheduler starts.
Task X (priority 5) is added and starts running.
Task Y (priority 5) is added. Priority of Y is not strictly greater than X.
Task X continues to run.
Task X finishes.
Task Y starts running.
Task Y finishes.

Output:
Task 10 (Priority 5) started.
Task 10 (Priority 5) finished.
Task 11 (Priority 5) started.
Task 11 (Priority 5) finished.

Example 3: Rapid High-Priority Additions

Input:
Tasks to add:
- Task P (ID: 100, Priority: 1)
- Task Q (ID: 101, Priority: 2)
- Task R (ID: 102, Priority: 3)
- Task S (ID: 103, Priority: 2)

Scheduler starts.
Task P (priority 1) added and starts.
Task Q (priority 2) added. Preempts P. Q starts.
Task R (priority 3) added. Preempts Q. R starts.
Task S (priority 2) added. Priority of S is not greater than R. S is queued.
Task R finishes.
Scheduler resumes Q (highest waiting priority).
Task Q finishes.
Scheduler resumes P (highest waiting priority).
Task P finishes.
Task S is next as it has priority 2, same as Q, but arrived later. (Or could have been executed before Q if logic favored it). Let's assume FIFO for same priority.

Output:
Task 100 (Priority 1) started.
Task 101 (Priority 2) added. Preempting Task 100.
Task 101 (Priority 2) started.
Task 102 (Priority 3) added. Preempting Task 101.
Task 102 (Priority 3) started.
Task 103 (Priority 2) added.
Task 102 (Priority 3) finished.
Task 101 (Priority 2) resumed.
Task 101 (Priority 2) finished.
Task 100 (Priority 1) resumed.
Task 100 (Priority 1) finished.
Task 103 (Priority 2) started.
Task 103 (Priority 2) finished.

Constraints

  • Task Priorities: Integers between 1 and 100 (inclusive).
  • Number of tasks: Up to 100 tasks can be added to the scheduler at any given time.
  • Task Duration: Individual task Run methods should simulate work over a period of 100-500 milliseconds.
  • Concurrency: The scheduler's internal state must be protected from race conditions.

Notes

  • You will need to think about how to signal a task to pause and resume. Channels are a common and idiomatic Go mechanism for this.
  • Consider using a data structure that efficiently allows you to find the highest-priority task (e.g., a priority queue or a sorted list).
  • The Task.Run method should be designed to be interruptible. For instance, it might periodically check a channel for a "pause" or "resume" signal.
  • The problem statement implies that tasks are executed in discrete steps or "ticks." You'll need to decide how granular this simulation should be. A simple approach is to have the Task.Run method print a "running" message and then sleep briefly, checking for preemption signals during that sleep.
  • Success will be measured by the correct execution order of tasks based on their priorities, ensuring that higher-priority tasks preempt lower-priority ones and that tasks are resumed correctly after preemption.
Loading editor...
go