Какие типичные причины 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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.