programming

Priority Queue C: Build A Fast Task Scheduler

If you're building a system that needs to process tasks by urgency rather than arrival order, a standard FIFO queue won't cut it. You need a priority queue C implementation. Unlike basic queues, a pri

Priority Queue C: Build A Fast Task Scheduler
In this story

If you're building a system that needs to process tasks by urgency rather than arrival order, a standard FIFO queue won't cut it. You need a priority queue C implementation. Unlike basic queues, a priority queue ensures that elements with higher urgency are extracted first, making it essential for event-driven simulations, pathfinding algorithms, and operating system kernels.

Standard C doesn't provide a built-in priority queue data structure. You have to design and build one yourself. This guide walks through implementing an efficient binary heap-based priority queue from scratch, managing dynamic memory, and writing robust scheduler code that avoids common performance traps.

Why C Developers Need Custom Priority Queues

Standard libraries in languages like C++ (std::priority_queue) or Python (heapq) handle these data structures automatically. In C, you're working directly with memory and raw pointers. While this requires more code up front, it gives you absolute control over memory allocation and execution speed.

In embedded systems or game loops, avoiding hidden heap allocations is critical. A custom array-backed binary heap lets you pre-allocate memory once at startup. That prevents runtime fragmentation during high-frequency task scheduling.

To understand the core design, let's look at the operational requirements:

  • Insertion (enqueue): Add a task with an associated priority value.
  • Extraction (dequeue): Remove and return the task with the highest priority.
  • Peek: Inspect the top task without removing it.

Designing a Binary Heap in C

While you can implement a priority queue using a sorted or unsorted linked list, those structures scale poorly. An unsorted list forces $O(n)$ removal time, while a sorted list forces $O(n)$ insertion time.

A binary heap solves this by providing $O(\log n)$ performance for both insertions and deletions, while keeping storage contiguous in memory. A binary heap is a complete binary tree stored inside a flat array. For any node at index $i$:

  • Its left child is located at index $2i + 1$.
  • Its right child is located at index $2i + 2$.
  • Its parent is located at index $(i - 1) / 2$.

Defining the Data Structures

First, define the structures for individual tasks and the priority queue container. We'll use a min-heap approach, where lower numeric values represent higher priority (e.g., priority 0 runs before priority 5).

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct {
    int id;
    int priority; // Lower number means higher priority
    char name[32];
} Task;

typedef struct {
    Task *items;
    int capacity;
    int size;
} PriorityQueue;

PriorityQueue* create_queue(int capacity) {
    PriorityQueue *pq = (PriorityQueue*)malloc(sizeof(PriorityQueue));
    if (!pq) return NULL;
    
    pq->items = (Task*)malloc(capacity * sizeof(Task));
    if (!pq->items) {
        free(pq);
        return NULL;
    }
    
    pq->capacity = capacity;
    pq->size = 0;
    return pq;
}

Implementing Core Heap Operations

To maintain the heap property—where every parent node has a higher priority (lower numerical value) than its children—we need two helper functions: swap, bubble-up (for insertions), and bubble-down (for extractions).

Swapping and Bubbling Up

When you insert a new task, you place it at the next available slot at the end of the array. To restore the heap property, you compare it against its parent and swap them if the child has a higher priority. You repeat this upward until the heap property holds.

static void swap(Task *a, Task *b) {
    Task temp = *a;
    *a = *b;
    *b = temp;
}

void pq_insert(PriorityQueue *pq, int id, int priority, const char *name) {
    if (pq->size >= pq->capacity) {
        // Handle dynamic resizing or return error
        int new_capacity = pq->capacity * 2;
        Task *temp = (Task*)realloc(pq->items, new_capacity * sizeof(Task));
        if (!temp) return;
        pq->items = temp;
        pq->capacity = new_capacity;
    }

    int i = pq->size++;
    pq->items[i].id = id;
    pq->items[i].priority = priority;
    snprintf(pq->items[i].name, sizeof(pq->items[i].name), "%s", name);

    // Bubble up
    while (i > 0 && pq->items[(i - 1) / 2].priority > pq->items[i].priority) {
        swap(&pq->items[i], &pq->items[(i - 1) / 2]);
        i = (i - 1) / 2;
    }
}

Extracting the Minimum Element

Removing the highest-priority task means taking the root element at index 0. To fill the gap, you move the last element in the array to the root and bubble it down by swapping it with its most urgent child until it finds its proper place.

bool pq_extract_min(PriorityQueue *pq, Task *out_task) {
    if (pq->size <= 0) return false;

    *out_task = pq->items[0];
    
    // Move last item to root and shrink size
    pq->items[0] = pq->items[pq->size - 1];
    pq->size--;

    int i = 0;
    while (true) {
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        int smallest = i;

        if (left < pq->size && pq->items[left].priority < pq->items[smallest].priority) {
            smallest = left;
        }
        if (right < pq->size && pq->items[right].priority < pq->items[smallest].priority) {
            smallest = right;
        }

        if (smallest != i) {
            swap(&pq->items[i], &pq->items[smallest]);
            i = smallest;
        } else {
            break;
        }
    }

    return true;
}

Building a Practical Task Scheduler

Now let's assemble these building blocks into a functional task scheduling loop. Real-world schedulers process background jobs, handle network packets, or manage render frames.

Here is how you initialize the queue, populate it with mixed-priority jobs, and execute them in correct order:

💡 You might also like: this article
int main(void) {
    PriorityQueue *scheduler = create_queue(10);
    if (!scheduler) return 1;

    // Enqueue tasks with varying priorities
    pq_insert(scheduler, 101, 5, "Background Backup");
    pq_insert(scheduler, 102, 1, "Critical UI Render");
    pq_insert(scheduler, 103, 3, "Network Packet Flush");
    pq_insert(scheduler, 104, 1, "User Input Polling");

    Task current_task;
    while (scheduler->size > 0) {
        if (pq_extract_min(scheduler, &current_task)) {
            printf("Executing Task ID: %d | Priority: %d | Name: %s\n", 
                current_task.id, current_task.priority, current_task.name);
        }
    }

    // Clean up memory
    free(scheduler->items);
    free(scheduler);
    return 0;
}

When you run this code, notice that tasks with priority 1 execute first, regardless of the order they were inserted. User Input Polling and Critical UI Render run before the Network Packet Flush and Background Backup.

Memory Management and Performance Pitfalls

Writing C code means you own the hardware lifecycle. Simple mistakes lead to memory leaks, buffer overflows, or undefined behavior. Keep these guidelines in mind:

  • Always free allocations: If your queue dynamically resizes using realloc, make sure you free the backing array and the queue pointer itself when shutting down.
  • Watch for integer overflow: When calculating child indices (2 * i + 1), ensure your loop counters don't overflow the integer range on embedded platforms.
  • Avoid deep copies of large payloads: Storing large structs directly inside the heap array degrades cache locality. Instead, store lightweight metadata or pointers, and keep the heavy task payloads in a separate memory pool. Read more about low-level memory layout strategies in the GNU C Library Manual.

Frequently Asked Questions

What is the time complexity of a binary heap priority queue?

Insertions and extractions run in $O(\log n)$ time. Peeking at the highest-priority element takes $O(1)$ time because it always resides at index 0.

Can I implement a priority queue with a linked list in C?

Yes, but it isn't recommended for performance-sensitive code. While inserting into an unsorted linked list is $O(1)$, extracting the highest priority requires scanning the entire list, resulting in $O(n)$ time complexity.

How do I handle tasks with identical priorities?

A standard binary heap doesn't guarantee stable sorting for equal priorities. If task arrival order matters for equal priorities, add a secondary sequence counter to your Task struct and incorporate it into your comparison logic.

When should I use a custom C priority queue over external libraries?

Write your own implementation when you are working on resource-constrained embedded systems, writing custom kernel modules, or aiming to minimize external dependencies. For general application programming where safety wrappers exist, consult language specifications like the ISO C Standard for compliant memory management patterns.

SB

Written by

Samuel Baker

Samuel Baker approaches each story with intellectual curiosity and a commitment to fairness, earning the trust of readers and sources alike.

More from programming