Планировщик задач на Rust: от простого таймера до промышленного job scheduler
Планировщик задач (job scheduler) — один из ключевых компонентов любого серьёзного приложения. На Rust вы можете построить систему, которая будет выполняться с точностью до миллисекунд, потреблять минимум памяти и работать без сбоев годами. В этой статье мы разберём, как создать планировщик задач на Rust, используя как стандартные средства, так и популярные крейты.
Зачем писать планировщик на Rust?
Rust предлагает уникальное сочетание производительности и безопасности. Планировщик задач на Rust:
- Гарантирует отсутствие гонок данных (data races) — компилятор проверяет это на этапе сборки.
- Работает без сборщика мусора — предсказуемая задержка выполнения задач.
- Поддерживает как асинхронное, так и многопоточное выполнение.
- Имеет минимальный потребляемый объём памяти — можно запускать тысячи задач.
Простейший планировщик с std::thread::sleep
Начнём с базового примера — циклическое выполнение задачи каждые N секунд:
use std::thread;
use std::time::Duration;
fn main() {
let interval = Duration::from_secs(5);
loop {
println!("[{}] Выполняю задачу...", chrono::Local::now().format("%H:%M:%S"));
// Здесь ваш код задачи
thread::sleep(interval);
}
}
Этот подход прост, но неэффективен: блокируется текущий поток, нет возможности запускать несколько задач параллельно.
Асинхронный планировщик на tokio
Для реальных проектов используйте tokio — асинхронный рантайм, который позволяет запускать тысячи задач в одном потоке.
use tokio::time::{interval, Duration};
#[tokio::main]
async fn main() {
let mut timer = interval(Duration::from_secs(10));
println!("Планировщик запущен. Задача будет выполняться каждые 10 секунд.");
loop {
timer.tick().await;
println!("[{}] Выполняю задачу...", chrono::Local::now().format("%H:%M:%S"));
// Асинхронная задача: например, запрос к API
do_work().await;
}
}
async fn do_work() {
// Имитация полезной работы
tokio::time::sleep(Duration::from_millis(500)).await;
println!("Задача завершена.");
}
Преимущества: не блокирует поток, можно параллельно запускать сотни таких планировщиков.
CRON-подобное расписание с крейтом cron
Если нужно задавать сложные расписания (как в Linux cron), используйте крейт cron:
use cron::Schedule;
use chrono::Utc;
use std::str::FromStr;
fn main() {
// Каждую минуту в 30 секунд
let expression = "30 * * * * *";
let schedule = Schedule::from_str(expression).unwrap();
println!("Следующие 5 запусков:");
for datetime in schedule.upcoming(Utc).take(5) {
println!(" {}", datetime);
}
}
Вот более полный пример с реальным выполнением:
use cron::Schedule;
use chrono::{Utc, Local};
use std::thread;
use std::time::Duration;
use std::str::FromStr;
fn main() {
// Каждые 15 секунд
let expression = "0/15 * * * * *";
let schedule = Schedule::from_str(expression).unwrap();
loop {
let now = Utc::now();
if let Some(next) = schedule.upcoming(Utc).next() {
let delay = next - now;
let millis = delay.num_milliseconds().max(0) as u64;
thread::sleep(Duration::from_millis(millis));
println!("[{}] Задача по расписанию!", Local::now().format("%H:%M:%S"));
// Выполнение задачи
} else {
thread::sleep(Duration::from_secs(1));
}
}
}
Промышленный планировщик с многопоточностью
Для серьёзных проектов создайте структуру Scheduler, которая управляет задачами в отдельных потоках:
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
type Job = Box<dyn Fn() + Send + Sync>;
struct ScheduledTask {
name: String,
interval: Duration,
last_run: Instant,
job: Arc<Job>,
}
pub struct Scheduler {
tasks: Arc<Mutex<Vec<ScheduledTask>>>,
running: Arc<std::sync::atomic::AtomicBool>,
}
impl Scheduler {
pub fn new() -> Self {
Scheduler {
tasks: Arc::new(Mutex::new(Vec::new())),
running: Arc::new(std::sync::atomic