RustMiddleTechnical

Какие типичные причины borrow checker ошибок в async Rust?

Async Rust borrow-ошибки чаще всего вызывают: non-Send типы (Rc, RefCell, std MutexGuard) через .await в tokio multi-thread runtime, хранение &-ссылок на локальные переменные через точки приостановки и self-referential Future без Pin.

Типичные причины ошибок borrow checker в async Rust

Async Rust добавляет к стандартным правилам borrow checker дополнительное ограничение: всё, что хранится в Future, должно реализовывать Send (если Future выполняется в multi-threaded runtime). Это порождает специфический класс ошибок.

1. Non-Send тип в Future (Send bound)

Tokio multi-thread runtime требует, чтобы Future были Send. Rc, RefCell, MutexGuard и raw pointers не являются Send.

use std::rc::Rc;
use tokio;

// ОШИБКА: Rc не Send
async fn broken() {
    let rc = Rc::new(42);
    tokio::time::sleep(std::time::Duration::from_millis(1)).await;
    println!("{}", rc); // rc пересекает .await — Future не Send
}

// ПРАВИЛЬНО: Arc вместо Rc
async fn fixed() {
    let arc = std::sync::Arc::new(42);
    tokio::time::sleep(std::time::Duration::from_millis(1)).await;
    println!("{}", arc);
}

2. MutexGuard через .await (deadlock + non-Send)

std::sync::MutexGuard не является Send и держит блокировку через точку приостановки.

use std::sync::Mutex;
use std::sync::Arc;

// ОШИБКА: MutexGuard через .await
async fn broken(data: Arc<Mutex<Vec<i32>>>) {
    let guard = data.lock().unwrap();
    some_async_fn().await; // guard живёт через await — не Send + потенциальный deadlock
    println!("{:?}", *guard);
}

// ПРАВИЛЬНО: используйте tokio::sync::Mutex или ограничьте lifetime guard
async fn fixed(data: Arc<tokio::sync::Mutex<Vec<i32>>>) {
    let guard = data.lock().await;
    some_async_fn().await; // tokio::MutexGuard реализует Send
    println!("{:?}", *guard);
}

// Или: явно drop guard до await
async fn fixed2(data: Arc<Mutex<Vec<i32>>>) {
    let value = {
        let guard = data.lock().unwrap();
        guard.clone() // берём данные, guard дропается здесь
    };
    some_async_fn().await;
    println!("{:?}", value);
}
async fn some_async_fn() {}

3. Заимствование локальной переменной через .await

Если Future хранит ссылку на локальную переменную, lifetime ссылки должен охватывать всю Future.

// ОШИБКА: ссылка на локальную переменную в async блоке
async fn broken() {
    let data = vec![1, 2, 3];
    let slice = &data[..]; // borrow начинается здесь
    some_async_fn().await;  // Future может быть приостановлена и перемещена
    println!("{:?}", slice);
}

// ПРАВИЛЬНО: передавайте owned значения или используйте Arc
async fn fixed() {
    let data = vec![1, 2, 3];
    some_async_fn().await;
    println!("{:?}", &data[..]); // borrow после await
}
async fn some_async_fn() {}

4. Self-referential структуры в Pin

Async state machine компилятора может создавать self-referential структуры. Без Pin перемещение Future инвалидирует внутренние указатели.

// Pin<Box<dyn Future>> нужен для хранения Future в struct
use std::pin::Pin;
use std::future::Future;

struct TaskQueue {
    // ОШИБКА: Box<dyn Future> можно переместить, инвалидируя self-references
    // tasks: Vec<Box<dyn Future<Output = ()>>>,
    
    // ПРАВИЛЬНО: Pin гарантирует, что Future не переместится
    tasks: Vec<Pin<Box<dyn Future<Output = ()>>>>,
}

5. Lifetime в async fn параметрах

// ОШИБКА: компилятор не может вывести lifetime для async fn с несколькими ссылками
async fn process<'a>(x: &'a str, y: &str) -> &'a str {
    x // lifetime y неявно привязывается, что может конфликтовать
}

// ПРАВИЛЬНО: явные lifetime annotations
async fn process<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x
}

Подводные камни

  • Использовать std::sync::Mutex вместо tokio::sync::Mutex в async контексте — первый не является Send через .await.
  • Держать MutexGuard через точку .await — блокирует executor thread в tokio single-thread runtime.
  • Передавать Rc, RefCell или raw pointers в spawned tasks — нарушает Send bound tokio::spawn.
  • Не использовать tokio::task::spawn_blocking для блокирующего кода — блокирует весь async executor.
  • Игнорировать ошибку «cannot be sent between threads safely» и пытаться добавить unsafe Send impl.
  • Не понимать разницу между futures::executor::block_on (single-thread) и tokio::runtime::Runtime (multi-thread) — Send требования разные.
  • Хранить ссылки в структурах, которые реализуют Future вручную, без Pin.
  • Не читать сообщение компилятора полностью: Rust указывает конкретную строку, где Future перестаёт быть Send.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics