Планировщик задач на C++: полное руководство
Планировщик задач (Task Scheduler) — это мощный инструмент, позволяющий автоматизировать выполнение кода в определённые моменты времени или с заданной периодичностью. В C++ реализация такого планировщика требует понимания многопоточности, синхронизации и работы с временными интервалами. В этой статье мы разберём, как создать собственный планировщик задач с нуля, используя стандартную библиотеку C++ (C++11 и новее).
Основные компоненты планировщика
Любой планировщик задач состоит из нескольких ключевых элементов:
- Хранилище задач — очередь или контейнер для хранения запланированных задач.
- Таймер — механизм, определяющий, когда должна быть выполнена задача.
- Пул потоков — для параллельного выполнения задач без блокировки основного потока.
- Механизм отмены — возможность отменить задачу до её выполнения.
Простая реализация планировщика с одним потоком
Начнём с базового примера, где задачи выполняются последовательно в фоновом потоке:
#include <iostream>
#include <thread>
#include <chrono>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>
class TaskScheduler {
public:
// Планирует задачу с задержкой
void schedule(std::function<void()> task, int delay_ms) {
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push({std::chrono::steady_clock::now() +
std::chrono::milliseconds(delay_ms), task});
cv_.notify_one();
}
// Запускает планировщик в отдельном потоке
void start() {
worker_ = std::thread([this]() {
while (!stop_) {
std::unique_lock<std::mutex> lock(mutex_);
if (tasks_.empty()) {
cv_.wait(lock, [this]() {
return !tasks_.empty() || stop_;
});
}
if (stop_) break;
auto now = std::chrono::steady_clock::now();
auto& top = tasks_.top();
if (top.time <= now) {
auto task = top.task;
tasks_.pop();
lock.unlock();
task(); // Выполняем задачу
} else {
cv_.wait_until(lock, top.time);
}
}
});
}
void stop() {
stop_ = true;
cv_.notify_all();
if (worker_.joinable()) worker_.join();
}
private:
struct Task {
std::chrono::steady_clock::time_point time;
std::function<void()> task;
bool operator>(const Task& other) const {
return time > other.time;
}
};
std::priority_queue<Task, std::vector<Task>, std::greater<Task>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
std::thread worker_;
bool stop_ = false;
};
int main() {
TaskScheduler scheduler;
scheduler.start();
scheduler.schedule([]() {
std::cout << "Задача 1 выполнена через 2 секунды\n";
}, 2000);
scheduler.schedule([]() {
std::cout << "Задача 2 выполнена через 1 секунду\n";
}, 1000);
std::this_thread::sleep_for(std::chrono::seconds(3));
scheduler.stop();
return 0;
}
Планировщик с пулом потоков для параллельного выполнения
Для более сложных сценариев, когда задачи могут выполняться долго, лучше использовать пул потоков. Вот пример с минимальным пулом:
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
class ThreadPool {
public:
ThreadPool(size_t threads) : stop_(false) {
for (size_t i = 0; i < threads; ++i) {
workers_.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
condition_.wait(lock, [this]() {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task();
}
});
}
}
template<class F, class... Args>
auto enqueue(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type> {
using return_type = typename std::result_of<F(Args...)>::type;
auto task = std::make_shared<std::packaged_task<return_type()>>(
std