Таймеры и обработка сигналов
Время и таймеры
- Интеграция со средой выполнения, не используйте thread::sleep и т.д.
- std Instant и Duration
- sleep
- interval
- timeout
- Специальный фьючерс vs select/race
Обработка сигналов
- Что такое обработка сигналов и почему это проблема для async?
- Сильно зависит от ОС
- См. документацию Tokio
Работа с временем и обработка сигналов — это важные аспекты асинхронного программирования, которые требуют специального подхода при использовании async/await.
Время и таймеры
Интеграция со средой выполнения, не используйте thread::sleep
В асинхронном программировании критически важно использовать асинхронные версии функций работы со временем, а не их синхронные аналоги:
#![allow(unused)] fn main() { // ПЛОХО - блокирует весь поток std::thread::sleep(Duration::from_secs(1)); // ХОРОШО - уступает управление планировщику tokio::time::sleep(Duration::from_secs(1)).await; }
Использование thread::sleep в асинхронной задаче блокирует весь поток ОС, предотвращая выполнение других задач на этом потоке.
std Instant и Duration
Типы из стандартной библиотеки std::time::Instant и std::time::Duration полностью совместимы с асинхронным кодом:
#![allow(unused)] fn main() { use std::time::{Instant, Duration}; async fn measure_performance() { let start = Instant::now(); // Асинхронная работа some_async_operation().await; let elapsed = start.elapsed(); println!("Операция заняла: {:?}", elapsed); } }
sleep
tokio::time::sleep — это асинхронная функция, которая приостанавливает выполнение текущей задачи на указанное время:
#![allow(unused)] fn main() { use tokio::time; async fn delayed_task() { println!("Начало задачи"); time::sleep(Duration::from_secs(2)).await; println!("Задача продолжена после 2 секунд"); } }
Важно: sleep не гарантирует точное время пробуждения, только минимальную задержку.
interval
interval создает периодический таймер, который "тикнул" через регулярные промежутки времени:
#![allow(unused)] fn main() { use tokio::time; async fn periodic_task() { let mut interval = time::interval(Duration::from_secs(1)); loop { interval.tick().await; // Ждет следующий "тик" println!("Прошла еще одна секунда"); // Выход по какому-то условию if should_stop() { break; } } } }
Особенности:
- Первый
tickзавершается немедленно - Последующие
tickждут до следующего интервала - Если обработка занимает больше времени, чем интервал, следующие "тики" будут пропущены
timeout
timeout оборачивает фьючерс и возвращает ошибку, если он не завершился за указанное время:
#![allow(unused)] fn main() { use tokio::time; async fn fetch_with_timeout() -> Result<String, Box<dyn std::error::Error>> { let result = time::timeout( Duration::from_secs(5), fetch_data_from_network() ).await?; Ok(result) } }
Специальный фьючерс vs select/race
timeout реализован как специальный фьючерс, а не просто комбинация sleep и select. Это дает несколько преимуществ:
С использованием timeout:
#![allow(unused)] fn main() { // Просто и понятно match time::timeout(Duration::from_secs(5), async_operation()).await { Ok(result) => println!("Успех: {:?}", result), Err(_) => println!("Таймаут"), } }
Эквивалент с select:
#![allow(unused)] fn main() { tokio::select! { result = async_operation() => { println!("Успех: {:?}", result); } _ = time::sleep(Duration::from_secs(5)) => { println!("Таймаут"); } } }
Преимущества timeout:
- Более чистый и выразительный код
- Лучшая обработка ошибок
- Проще в использовании и понимании
Обработка сигналов
Что такое обработка сигналов и почему это проблема для async?
Сигналы — это механизм в Unix-подобных системах для уведомления процессов о различных событиях (например, SIGINT для прерывания, SIGTERM для завершения).
Проблемы с обработкой сигналов в async:
- Блокирующий характер: Традиционные обработчики сигналов блокируют поток
- Интеграция с event loop: Сигналы должны быть интегрированы в цикл событий среды выполнения
- Портабельность: Разные ОС имеют разные API для работы с сигналами
Сильно зависит от ОС
Обработка сигналов значительно различается между операционными системами:
Unix-системы (Linux, macOS, BSD):
#![allow(unused)] fn main() { #[cfg(unix)] mod unix_example { use tokio::signal::unix::{signal, SignalKind}; pub async fn handle_unix_signals() -> Result<(), Box<dyn std::error::Error>> { let mut sigterm = signal(SignalKind::terminate())?; let mut sigint = signal(SignalKind::interrupt())?; tokio::select! { _ = sigterm.recv() => { println!("Получен SIGTERM"); } _ = sigint.recv() => { println!("Получен SIGINT"); } } Ok(()) } } }
Windows:
#![allow(unused)] fn main() { #[cfg(windows)] mod windows_example { use tokio::signal::windows; pub async fn handle_windows_signals() -> Result<(), Box<dyn std::error::Error>> { let mut ctrl_c = windows::ctrl_c()?; ctrl_c.recv().await; println!("Получен CTRL+C"); Ok(()) } } }
Кросс-платформенная обработка сигналов
Tokio предоставляет кросс-платформенные API для распространенных сценариев:
#![allow(unused)] fn main() { use tokio::signal; async fn graceful_shutdown() { // Ожидание CTRL+C (Unix) или CTRL+C/CTRL+BREAK (Windows) signal::ctrl_c() .await .expect("Не удалось настроить обработчик сигнала"); println!("Получен сигнал завершения, начинаем graceful shutdown"); // Здесь можно освободить ресурсы, сохранить состояние и т.д. perform_cleanup().await; println("Завершение работы"); } }
Практический пример: Graceful Shutdown
use tokio::{signal, sync::mpsc, time}; use std::time::Duration; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let (shutdown_tx, mut shutdown_rx) = mpsc::channel(1); // Запускаем фоновую задачу let worker = tokio::spawn(async move { while let Some(_) = shutdown_rx.recv().await { println!("Получена команда завершения"); break; } println!("Рабочая задача завершена"); }); // Ожидаем сигнал завершения tokio::select! { _ = signal::ctrl_c() => { println!("\nПолучен CTRL+C, инициируем завершение..."); } _ = time::sleep(Duration::from_secs(30)) => { println!("Таймаут, инициируем завершение..."); } } // Отправляем команду завершения let _ = shutdown_tx.send(()).await; // Ждем завершения рабочей задачи let _ = worker.await; println!("Приложение завершено корректно"); Ok(()) }
См. документацию Tokio
Для более сложных сценариев обработки сигналов и специфичных для ОС функций обратитесь к официальной документации Tokio: