TopicsQueueTask Scheduling
📖

Task Scheduling

Queue
~20 min read
5 practice problems

Overview

Task scheduling using queues is a fundamental problem in computer science where tasks, jobs, or processes need to be managed and executed in a specific order. Queues provide the perfect data structure for maintaining task order and ensuring fair processing.

Task scheduling problems often involve:

  • FIFO scheduling - Process tasks in arrival order
  • Round-robin scheduling - Cycle through tasks fairly
  • Priority scheduling - Process tasks by priority
  • Task dependencies - Handle tasks that depend on others
  • Resource constraints - Limited resources for task execution

Key characteristics:

  • Order maintenance - Queue maintains task order
  • Fair processing - Tasks processed in arrival order
  • Resource management - Manage limited resources
  • Dependency handling - Handle task dependencies
  • Efficient execution - O(1) enqueue/dequeue operations

Basic Task Scheduler

Simple FIFO task scheduler:

interface Task {
  id: number;
  name: string;
  execute: () => void;
}

class TaskScheduler {
  private queue: Task[] = [];
  private isProcessing = false;
  
  addTask(task: Task): void {
    this.queue.push(task);
    if (!this.isProcessing) {
      this.processNext();
    }
  }
  
  private async processNext(): Promise<void> {
    if (this.queue.length === 0) {
      this.isProcessing = false;
      return;
    }
    
    this.isProcessing = true;
    const task = this.queue.shift()!;
    
    try {
      task.execute();
    } catch (error) {
      console.error(`Task ${task.id} failed:`, error);
    }
    
    // Process next task
    await this.processNext();
  }
  
  getQueueSize(): number {
    return this.queue.length;
  }
  
  isEmpty(): boolean {
    return this.queue.length === 0;
  }
}

Time: O(1) enqueue, O(n) dequeue (if using shift), Space: O(n)

Key insight: Queue maintains task order for FIFO processing.


Pattern 1: Round-Robin Scheduling

Process tasks in round-robin fashion:

class RoundRobinScheduler {
  private queue: Task[] = [];
  private timeSlice: number;
  
  constructor(timeSlice: number = 100) {
    this.timeSlice = timeSlice; // milliseconds per task
  }
  
  addTask(task: Task): void {
    this.queue.push(task);
  }
  
  async schedule(): Promise<void> {
    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      
      // Execute task for time slice
      const startTime = Date.now();
      while (Date.now() - startTime < this.timeSlice && !task.isComplete()) {
        task.executeStep();
      }
      
      // If not complete, add back to queue
      if (!task.isComplete()) {
        this.queue.push(task);
      }
    }
  }
}

Time: O(n × timeSlice), Space: O(n)

Key insight: Cycle through tasks, giving each a time slice.


Pattern 2: Task Scheduling with Dependencies

Schedule tasks respecting dependencies:

function scheduleTasks(tasks: Task[], dependencies: number[][]): number[] {
  const n = tasks.length;
  const inDegree = new Array(n).fill(0);
  const graph: number[][] = new Array(n).fill(0).map(() => []);
  
  // Build graph and calculate in-degrees
  for (const [task, dep] of dependencies) {
    graph[dep].push(task);
    inDegree[task]++;
  }
  
  // Add tasks with no dependencies to queue
  const queue: number[] = [];
  for (let i = 0; i < n; i++) {
    if (inDegree[i] === 0) {
      queue.push(i);
    }
  }
  
  const result: number[] = [];
  
  while (queue.length > 0) {
    const task = queue.shift()!;
    result.push(task);
    
    // Process dependent tasks
    for (const dependent of graph[task]) {
      inDegree[dependent]--;
      if (inDegree[dependent] === 0) {
        queue.push(dependent);
      }
    }
  }
  
  // Check if all tasks scheduled (no cycles)
  return result.length === n ? result : [];
}

Time: O(V + E), Space: O(V + E)

Key insight: Use topological sort with queue for dependency resolution.


Pattern 3: Task Scheduling with Cooldown

Schedule tasks with cooldown period:

function leastInterval(tasks: string[], n: number): number {
  const freq = new Map<string, number>();
  
  // Count task frequencies
  for (const task of tasks) {
    freq.set(task, (freq.get(task) || 0) + 1);
  }
  
  const maxFreq = Math.max(...Array.from(freq.values()));
  const maxCount = Array.from(freq.values()).filter(f => f === maxFreq).length;
  
  // Calculate minimum intervals
  return Math.max(
    tasks.length,
    (maxFreq - 1) * (n + 1) + maxCount
  );
}

// Using queue for actual scheduling
class TaskSchedulerWithCooldown {
  private queue: [string, number][] = []; // [task, nextAvailableTime]
  private cooldown: number;
  private taskFreq = new Map<string, number>();
  
  constructor(cooldown: number) {
    this.cooldown = cooldown;
  }
  
  schedule(tasks: string[]): number {
    // Count frequencies
    for (const task of tasks) {
      this.taskFreq.set(task, (this.taskFreq.get(task) || 0) + 1);
    }
    
    // Sort by frequency (use priority queue in practice)
    const sortedTasks = Array.from(this.taskFreq.entries())
      .sort((a, b) => b[1] - a[1]);
    
    let time = 0;
    
    while (sortedTasks.length > 0 || this.queue.length > 0) {
      // Process tasks that are ready
      if (this.queue.length > 0 && this.queue[0][1] <= time) {
        const [task] = this.queue.shift()!;
        const freq = this.taskFreq.get(task)! - 1;
        
        if (freq > 0) {
          this.taskFreq.set(task, freq);
          this.queue.push([task, time + this.cooldown + 1]);
        }
      }
      
      // Add new task if available
      if (sortedTasks.length > 0) {
        const [task, freq] = sortedTasks.shift()!;
        this.taskFreq.set(task, freq - 1);
        
        if (freq > 1) {
          this.queue.push([task, time + this.cooldown + 1]);
        }
      }
      
      time++;
    }
    
    return time;
  }
}

Time: O(n), Space: O(n)

Key insight: Track next available time for each task.


Pattern 4: CPU Task Scheduler

Simulate CPU task scheduling:

interface CPUTask {
  id: number;
  arrivalTime: number;
  burstTime: number;
  priority?: number;
}

class CPUScheduler {
  private readyQueue: CPUTask[] = [];
  private currentTime = 0;
  
  addTask(task: CPUTask): void {
    this.readyQueue.push(task);
    // Sort by priority or arrival time
    this.readyQueue.sort((a, b) => 
      (a.priority || 0) - (b.priority || 0) || 
      a.arrivalTime - b.arrivalTime
    );
  }
  
  schedule(): CPUTask[] {
    const completed: CPUTask[] = [];
    
    while (this.readyQueue.length > 0) {
      const task = this.readyQueue.shift()!;
      
      // Wait if task hasn't arrived
      if (this.currentTime < task.arrivalTime) {
        this.currentTime = task.arrivalTime;
      }
      
      // Execute task
      this.currentTime += task.burstTime;
      completed.push(task);
    }
    
    return completed;
  }
  
  getCurrentTime(): number {
    return this.currentTime;
  }
}

Time: O(n log n) for sorting, Space: O(n)

Key insight: Maintain ready queue sorted by priority/arrival time.


Pattern 5: Parallel Task Scheduling

Schedule tasks on multiple processors:

class ParallelTaskScheduler {
  private queues: Task[][] = [];
  private numProcessors: number;
  
  constructor(numProcessors: number) {
    this.numProcessors = numProcessors;
    this.queues = new Array(numProcessors).fill(0).map(() => []);
  }
  
  addTask(task: Task, processorId?: number): void {
    if (processorId !== undefined) {
      this.queues[processorId].push(task);
    } else {
      // Add to shortest queue (load balancing)
      const shortestQueue = this.queues.reduce((min, q, i) => 
        q.length < this.queues[min].length ? i : min, 0
      );
      this.queues[shortestQueue].push(task);
    }
  }
  
  async schedule(): Promise<void> {
    const processors = this.queues.map((queue, id) => 
      this.processQueue(queue, id)
    );
    
    await Promise.all(processors);
  }
  
  private async processQueue(queue: Task[], processorId: number): Promise<void> {
    while (queue.length > 0) {
      const task = queue.shift()!;
      await task.execute();
    }
  }
}

Time: O(n / processors), Space: O(n)

Key insight: Distribute tasks across multiple queues for parallel processing.


Pattern 6: LeetCode - Task Scheduler

LeetCode problem: schedule tasks with cooldown:

function leastInterval(tasks: string[], n: number): number {
  const freq = new Map<string, number>();
  
  // Count frequencies
  for (const task of tasks) {
    freq.set(task, (freq.get(task) || 0) + 1);
  }
  
  // Find max frequency
  const frequencies = Array.from(freq.values());
  const maxFreq = Math.max(...frequencies);
  const maxCount = frequencies.filter(f => f === maxFreq).length;
  
  // Calculate minimum time
  return Math.max(
    tasks.length,
    (maxFreq - 1) * (n + 1) + maxCount
  );
}

Time: O(n), Space: O(1)

Key insight: Most frequent task determines minimum time.


Pattern 7: Job Queue with Retry

Task scheduler with retry mechanism:

interface Job {
  id: number;
  execute: () => Promise<void>;
  maxRetries: number;
  retryCount: number;
}

class JobQueueWithRetry {
  private queue: Job[] = [];
  private failedQueue: Job[] = [];
  
  addJob(job: Job): void {
    this.queue.push(job);
  }
  
  async processJobs(): Promise<void> {
    while (this.queue.length > 0) {
      const job = this.queue.shift()!;
      
      try {
        await job.execute();
      } catch (error) {
        job.retryCount++;
        
        if (job.retryCount < job.maxRetries) {
          // Retry: add back to queue
          this.queue.push(job);
        } else {
          // Max retries reached: move to failed queue
          this.failedQueue.push(job);
        }
      }
    }
  }
  
  getFailedJobs(): Job[] {
    return this.failedQueue;
  }
}

Time: O(n × retries), Space: O(n)

Key insight: Retry failed tasks up to max retries.


When to Use Task Scheduling with Queues

Use queue-based task scheduling when you see:

  • "Schedule tasks" - Need to manage task execution order
  • "Round-robin" - Fair task distribution
  • "FIFO processing" - Process tasks in arrival order
  • "Task queue" - Maintain queue of pending tasks
  • "Job scheduling" - Schedule jobs with dependencies
  • "Resource management" - Manage limited resources
  • "Cooldown" - Tasks with cooldown periods

Key distinction:

  • Queue-based scheduling - FIFO order, simple and fair
  • Priority queue scheduling - Priority-based order
  • Round-robin - Fair time-sliced scheduling

Task Scheduling Patterns

| Pattern | Description | Use Case |

|---------|-------------|----------|

| FIFO | First in, first out | Simple task processing |

| Round-Robin | Cycle through tasks | Fair CPU scheduling |

| Priority | Process by priority | Important tasks first |

| Dependencies | Respect task dependencies | Build systems, workflows |

| Cooldown | Enforce cooldown periods | Rate limiting, API calls |

| Parallel | Multiple processors | Distributed systems |

Remember:

  • Queue for FIFO task scheduling
  • Priority queue for priority-based scheduling
  • Round-robin for fair time-sliced scheduling

Template (Basic Task Scheduler)

class TaskSchedulerTemplate<T> {
  private queue: T[] = [];
  
  addTask(task: T): void {
    this.queue.push(task);
  }
  
  processNext(): T | undefined {
    return this.queue.shift();
  }
  
  processAll(): T[] {
    const result: T[] = [];
    while (this.queue.length > 0) {
      result.push(this.queue.shift()!);
    }
    return result;
  }
  
  isEmpty(): boolean {
    return this.queue.length === 0;
  }
  
  size(): number {
    return this.queue.length;
  }
}

Template (Round-Robin Scheduler)

class RoundRobinTemplate<T> {
  private queue: T[] = [];
  private timeSlice: number;
  
  constructor(timeSlice: number) {
    this.timeSlice = timeSlice;
  }
  
  addTask(task: T): void {
    this.queue.push(task);
  }
  
  schedule(): void {
    while (this.queue.length > 0) {
      const task = this.queue.shift()!;
      
      // Process task for time slice
      // processTask(task, this.timeSlice);
      
      // If not complete, add back
      // if (!task.isComplete()) {
      //   this.queue.push(task);
      // }
    }
  }
}

Time and Space Complexity

Basic Task Scheduler

  • addTask: O(1)
  • processNext: O(1) with linked list, O(n) with array shift
  • processAll: O(n)
  • Space: O(n)

Round-Robin Scheduler

  • addTask: O(1)
  • schedule: O(n × timeSlice)
  • Space: O(n)

Task Scheduling with Dependencies

  • Time: O(V + E) where V is tasks, E is dependencies
  • Space: O(V + E)

Practice Tips

  1. Choose right structure - Queue for FIFO, priority queue for priority
  2. Handle dependencies - Use topological sort for dependencies
  3. Time management - Track current time for scheduling
  4. Resource constraints - Limit concurrent tasks
  5. Error handling - Handle task failures gracefully
  6. Retry logic - Implement retry for failed tasks
  7. Load balancing - Distribute tasks across processors

Common Mistakes

  1. Using shift() - O(n) operation, use linked list instead
  2. Not handling dependencies - Forgetting to check dependencies
  3. Race conditions - Not handling concurrent access
  4. Infinite loops - Circular dependencies in task graph
  5. Resource exhaustion - Not limiting concurrent tasks
  6. Time tracking - Not tracking time correctly
  7. Priority confusion - Wrong priority ordering

Real-World Applications

Task scheduling with queues is used extensively in:

  • Operating Systems - Process scheduling, CPU scheduling
  • Web Servers - Request handling, connection pools
  • Database Systems - Transaction scheduling
  • Distributed Systems - Job queues, task distribution
  • Build Systems - Dependency resolution, parallel builds
  • CI/CD Pipelines - Job scheduling, workflow management
  • Message Queues - RabbitMQ, Kafka, AWS SQS
  • Game Engines - Task scheduling, AI decision making

Related Concepts

  • Queue - Data structure for task scheduling
  • Priority Queue - Priority-based task scheduling
  • Topological Sort - Dependency resolution
  • Round-Robin - Fair scheduling algorithm
  • Load Balancing - Distributing tasks

Summary

Task scheduling using queues is fundamental for:

  • Order maintenance - Queue maintains task order
  • Fair processing - FIFO ensures fair task processing
  • Resource management - Manage limited resources
  • Dependency handling - Handle task dependencies
  • Efficient execution - O(1) operations for task management

Key takeaways:

  • Use queue for FIFO task scheduling
  • Handle dependencies with topological sort
  • Implement round-robin for fair scheduling
  • Track time for time-based scheduling
  • Essential for operating systems and distributed systems

Mastering task scheduling with queues will help you solve scheduling problems, manage resources efficiently, and design robust systems in coding interviews.