Уничтожение и очистка

  • Уничтожение объектов и повторение Drop
  • Общие требования к очистке в программном обеспечении
  • Проблемы с async
    • Может потребоваться делать что-то асинхронно во время очистки, например, отправить финальное сообщение
    • Может потребоваться очистить что-то, что все еще используется асинхронно
    • Может потребоваться очистка при завершении или отмене асинхронной задачи, и нет способа перехватить это
    • Состояние среды выполнения во время фазы очистки (особенно если мы паникуем или что-то подобное)
    • Нет асинхронного Drop
      • В работе (WIP)
    • Ссылка вперед на тему completion io

Отмена (Cancellation)

  • Как это происходит (повторение из more-async-await.md)
    • удаление (drop) фьючерса
    • токен отмены (cancellation token)
    • функции прерывания (abort)
  • Что мы можем сделать для «перехвата» отмены
    • логирование или мониторинг отмены
  • Как отмена влияет на другие фьючерсы и задачи (ссылка вперед на главу о безопасности отмены, здесь должно быть просто предупреждение)

Паника (Panicking) и async

  • Распространение паники между задачами (результат spawn)
  • Паника, оставляющая данные в несогласованном состоянии (мьютексы Tokio)
  • Вызов асинхронного кода при панике (убедитесь, что вы этого не делаете)

Паттерны для очистки

  • Избегание необходимости очистки (abort/restart)
  • Не использовать async для очистки и не беспокоиться слишком сильно
  • Асинхронный метод очистки + "dtor bomb" (т.е., отделение очистки от уничтожения)
  • Централизация/аутсорсинг очистки в отдельной задаче, потоке или объекте-супервизоре/процессе
  • https://tokio.rs/tokio/topics/shutdown

Почему (пока) нет асинхронного Drop

  • Примечание: это продвинутый раздел, и его чтение не обязательно
  • Почему асинхронный Drop сложен
  • Возможные решения и их проблемы
  • Текущий статус

Асинхронное программирование вводит уникальные проблемы, когда дело доходит до уничтожения объектов и очистки ресурсов. В этом разделе мы рассмотрим эти проблемы и существующие подходы к их решению.

Уничтожение объектов и повторение Drop

В Rust деструкторы реализуются с помощью трейта Drop. Когда объект выходит из области видимости, автоматически вызывается его метод drop:

#![allow(unused)]
fn main() {
impl Drop for MyStruct {
    fn drop(&mut self) {
        // Синхронная очистка ресурсов
        println!("Object is being destroyed");
    }
}
}

Однако метод drop является синхронным и не может быть асинхронным. Это создает фундаментальное ограничение для асинхронной очистки.

Общие требования к очистке в программном обеспечении

Типичные сценарии очистки включают:

  • Закрытие сетевых соединений
  • Сохранение состояния на диск
  • Освобождение блокировок и ресурсов
  • Уведомление других компонентов системы

Проблемы с async

Асинхронные операции при очистке

Часто требуется выполнять асинхронные операции во время очистки:

#![allow(unused)]
fn main() {
// НЕ РАБОТАЕТ - Drop не может быть async
impl Drop for DatabaseConnection {
    async fn drop(&mut self) { // ❌ Не компилируется
        self.send_final_metrics().await;
        self.close_gracefully().await;
    }
}
}

Очистка используемых асинхронно ресурсов

Ресурсы могут все использоваться асинхронными задачами в момент, когда требуется их очистка, что требует координации.

Очистка при завершении задач

Асинхронные задачи могут завершаться или отменяться без явного вызова методов очистки.

Состояние среды выполнения

Во время очистки (особенно при панике) среда выполнения может находиться в нестабильном состоянии, что затрудняет выполнение асинхронных операций.

Отмена (Cancellation)

Как происходит отмена

  1. Удаление фьючерса: Когда фьючерс удаляется (выходит из области видимости), он отменяется
  2. Токены отмены: Явная сигнализация через CancellationToken
  3. Функции прерывания: Вызов abort() на JoinHandle

Перехват отмены

Хотя нельзя "перехватить" отмену напрямую, можно использовать паттерны для логирования и мониторинга:

#![allow(unused)]
fn main() {
async fn critical_operation(cancel: CancellationToken) -> Result<()> {
    tokio::select! {
        result = async {
            // Основная работа
            do_work().await
        } => result,
        _ = cancel.cancelled() => {
            log::warn!("Operation was cancelled");
            Err(Error::Cancelled)
        }
    }
}
}

Паника (Panicking) и async

Распространение паники между задачами

Когда задача, порожденная с помощью tokio::spawn, паникует, паника не распространяется автоматически на родительскую задачу. Вместо этого JoinHandle возвращает Err при await:

#![allow(unused)]
fn main() {
let handle = tokio::spawn(async {
    panic!("This panic is caught by the runtime");
});

match handle.await {
    Ok(result) => println!("Task succeeded: {:?}", result),
    Err(err) => println!("Task panicked: {:?}", err), // Сработает здесь
}
}

Несогласованность данных

Паника может оставить данные в мьютексах в несогласованном состоянии. В отличие от std::sync::Mutex, мьютексы Tokio не отравляются при панике.

Вызов async кода при панике

Выполнение асинхронного кода во время паники (например, в деструкторе) крайне опасно, так как среда выполнения может находиться в нестабильном состоянии.

Паттерны для очистки

Избегание необходимости очистки

В некоторых системах проще перезапустить процесс/сервис, чем пытаться очистить сложное состояние.

Синхронная очистка

Для многих случаев достаточно синхронной очистки. Если асинхронные операции не критичны, можно просто игнорировать их при очистке.

Асинхронный метод очистки + "dtor bomb"

Отделение асинхронной очистки от деструктора:

#![allow(unused)]
fn main() {
struct Resource {
    // поля ресурса
    cleaned_up: bool,
}

impl Resource {
    async fn cleanup(&mut self) -> Result<()> {
        // Асинхронная очистка
        self.send_final_message().await?;
        self.cleaned_up = true;
        Ok(())
    }
}

impl Drop for Resource {
    fn drop(&mut self) {
        if !self.cleaned_up {
            // "Бомба" - логируем ошибку или паникуем
            eprintln!("WARNING: Resource was not properly cleaned up");
            // Или: panic!("Resource must be cleaned up before drop");
        }
    }
}
}

Централизованная очистка

Создание отдельной задачи или сервиса для управления очисткой:

#![allow(unused)]
fn main() {
struct CleanupManager {
    shutdown_tx: tokio::sync::mpsc::Sender<CleanupCommand>,
}

impl CleanupManager {
    async fn shutdown(&self) {
        // Отправляем команду на очистку всем компонентам
        let _ = self.shutdown_tx.send(CleanupCommand::Shutdown).await;
    }
}
}

Почему (пока) нет асинхронного Drop

Почему асинхронный Drop сложен

  1. Семантика времени жизни: Асинхронные операции требуют активной среды выполнения, которая может быть недоступна во время уничтожения
  2. Порядок уничтожения: Традиционный порядок LIFO (стек) разрушается при асинхронных операциях
  3. Ошибки при очистке: Обработка ошибок в деструкторах уже сложна, а асинхронность усугубляет проблему
  4. Интеграция с существующей экосистемой: Изменение поведения Drop затронуло бы всю экосистему Rust

Возможные решения и их проблемы

  1. Новый трейт AsyncDrop: Параллельный трейт для асинхронной очистки, но это создает дублирование API
  2. Изменение семантики Drop: Сделать Drop асинхронным, но это ломающее изменение
  3. Макросы или атрибуты: Генерация кода для обработки асинхронной очистки, но это может быть неэргономично

Текущий статус

На момент написания, асинхронный Drop все еще находится на стадии исследований и предложений. Ведутся активные обсуждения в рабочих группах Rust, но стабильного решения еще не представлено.

Рекомендация: Используйте явные методы асинхронной очистки (как показано в паттернах выше) и убедитесь, что критические ресурсы должным образом очищаются до уничтожения объектов.