Таймеры и обработка сигналов

Время и таймеры

  • Интеграция со средой выполнения, не используйте 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:

  1. Блокирующий характер: Традиционные обработчики сигналов блокируют поток
  2. Интеграция с event loop: Сигналы должны быть интегрированы в цикл событий среды выполнения
  3. Портабельность: Разные ОС имеют разные 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: