Планировщик задач на C++: архитектура, реализация и примеры
Планировщик задач (Task Scheduler) — один из ключевых компонентов современных высоконагруженных систем, игровых движков, серверов приложений и embedded-устройств. В этой статье мы подробно разберём, как создать собственный планировщик задач на C++ с нуля, используя стандартную библиотеку языка. Вы узнаете, как устроена очередь задач, как работают таймеры, как синхронизировать потоки и избежать типичных ошибок.
Зачем нужен планировщик задач?
Планировщик задач позволяет:
- асинхронно выполнять код в фоновых потоках;
- задавать задержки и периодические запуски;
- управлять приоритетами и порядком выполнения;
- оптимизировать использование ресурсов CPU.
В отличие от простого std::thread, планировщик даёт гибкий контроль над выполнением и позволяет избежать создания потоков под каждую задачу.
Базовая архитектура
Любой планировщик состоит из трёх основных частей:
- Очередь задач — хранит объекты задач (функции, аргументы, метаданные);
- Пул потоков — один или несколько рабочих потоков, которые извлекают задачи из очереди и выполняют их;
- Таймер / планировщик времени — позволяет откладывать выполнение или запускать задачи по расписанию.
Реализация очереди задач на C++
Для начала определим структуру задачи:
struct Task {
std::function<void()> func;
std::chrono::steady_clock::time_point executeAt;
size_t id;
};
Очередь должна быть потокобезопасной. Используем std::priority_queue (чтобы задачи с меньшим временем выполнения извлекались первыми) и std::mutex для синхронизации:
#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();
}
// Добавить задачу с задержкой
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();
} else {
// Ждём до времени выполнения задачи
cv.wait_until(lock, top.executeAt);
}
}
}
}
std::priority_queue<Task, std::vector<Task>, std::greater<Task>> queue;
std::mutex mutex;
std::condition_variable cv;
std::vector<std::thread> workers;
std::atomic<bool> stopping{false};
std::atomic<size_t> nextId{0};
};
Пример использования
Теперь создадим планировщик и добавим несколько задач с разными задержками:
#include <iostream>
#include <chrono>
#include <thread>
void taskA() {
std::cout << "Task A executed at " << std::chrono