Task Scheduler in C++: Architecture, Implementation, and Examples
A Task Scheduler is one of the key components of modern high-load systems, game engines, application servers, and embedded devices. In this article, we will thoroughly examine how to create your own task scheduler in C++ from scratch using the standard library. You will learn how a task queue works, how timers operate, how to synchronize threads, and how to avoid common mistakes.
Why do you need a Task Scheduler?
A Task Scheduler allows you to:
- asynchronously execute code in background threads;
- set delays and periodic launches;
- manage priorities and execution order;
- optimize CPU resource usage.
Unlike a simple std::thread, a scheduler provides flexible control over execution and helps avoid creating a thread for each task.
Basic Architecture
Any scheduler consists of three main parts:
- Task Queue — stores task objects (functions, arguments, metadata);
- Thread Pool — one or more worker threads that retrieve tasks from the queue and execute them;
- Timer / Time Scheduler — allows delaying execution or running tasks on a schedule.
Implementing a Task Queue in C++
First, let's define the task structure:
struct Task { std::function<void()> func; std::chrono::steady_clock::time_point executeAt; size_t id;};The queue must be thread-safe. We use std::priority_queue (so that tasks with a smaller execution time are retrieved first) and std::mutex for synchronization:
#include <queue>#include <mutex>#include <condition_variable>#include <functional>#include <chrono>#include <thread>#include <vector>
class TaskScheduler {public: explicit TaskScheduler(size_t threadCount = std::thread::hardware_concurrency()) { for (size_t i = 0; i < threadCount; ++i) { workers.emplace_back([this] { workerLoop(); }); } }
~TaskScheduler() { stop(); }
// Add a task with a delay template<typename F, typename... Args> void schedule(F&& f, Args&&... args, std::chrono::milliseconds delay = std::chrono::milliseconds(0)) { auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...); Task t{std::move(task), std::chrono::steady_clock::now() + delay, nextId++}; { std::lock_guard<std::mutex> lock(mutex); queue.push(std::move(t)); } cv.notify_one(); }
void stop() { { std::lock_guard<std::mutex> lock(mutex); stopping = true; } cv.notify_all(); for (auto& worker : workers) { if (worker.joinable()) worker.join(); } }
private: struct Task { std::function<void()> func; std::chrono::steady_clock::time_point executeAt; size_t id;
bool operator>(const Task& other) const { return executeAt > other.executeAt; } };
void workerLoop() { while (true) { std::unique_lock<std::mutex> lock(mutex); cv.wait(lock, [this] { return stopping || !queue.empty(); }); if (stopping && queue.empty()) return;
if (!queue.empty()) { auto top = queue.top(); if (std::chrono::steady_clock::now() >= top.executeAt) { queue.pop(); lock.unlock(); top.func