async
/.await
В первой главе мы бросили беглый взгляд на async
/.await
. В этой главе мы обсудим async
/.await
более подробно, объясняя, как они работают и чем async
-код отличается от традиционных программ на Rust.
async
/.await
- это специальный синтаксис Rust, который позволяет не блокировать поток, а передавать управление другому коду, пока ожидается завершение операции.
Существует два основных способа использования async
: async fn
и async
-блоки. Каждый возвращает значение, реализующее типаж Future
:
// `foo()` returns a type that implements `Future<Output = u8>`.
// `foo().await` will result in a value of type `u8`.
async fn foo() -> u8 { 5 }
fn bar() -> impl Future<Output = u8> {
// This `async` block results in a type that implements
// `Future<Output = u8>`.
async {
let x: u8 = foo().await;
x + 5
}
}
Как мы видели в первой главе, async
-блоки и другие футуры ленивы: они ничего не делают, пока их не запустят. Наиболее распространённый способ запустить Future
- это .await
. Когда .await
вызывается на Future
, он пытается выполнить её до конца. Если Future
заблокирована, то контроль будет передан текущему потоку. Чтобы добиться большего прогресса, будет выбрана верхняя Future
исполнителя, позволяя .await
продолжить работу.
Времена жизни async
В отличие от традиционных функций, async fn
, которые принимают ссылки или другие не-'static
аргументы, возвращают Future
, которая ограничена временем жизни аргумента:
// This function:
async fn foo(x: &u8) -> u8 { *x }
// Is equivalent to this function:
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
async move { *x }
}
Это означает, что для футуры, возвращаемая из async fn
, должен быть вызван .await
до тех пор, пока её не-'static
аргументы все ещё действительны. В общем случае, вызов .await
у футуры сразу после вызова функции (как в foo(&x).await
), не является проблемой. Однако, проблемой может оказаться сохранение футуры или отправка её в другую задачу или поток.
Один общий обходной путь для включения async fn
со ссылками в аргументах в 'static
футуру состоит в том, чтобы связать аргументы с вызовом async fn
внутри async
-блока:
fn bad() -> impl Future<Output = u8> {
let x = 5;
borrow_x(&x) // ERROR: `x` does not live long enough
}
fn good() -> impl Future<Output = u8> {
async {
let x = 5;
borrow_x(&x).await
}
}
Перемещая аргумент в async
-блок, мы продлеваем его время жизни до времени жизни Future
, которая возвращается при вызове good
.
async move
async
-блоки и async
-замыкания позволяют использовать ключевое слово move
, как и в обычном замыкании. async move
блок получает владение переменными со ссылками, позволяя им пережить текущую область, но отказывая им в возможности делиться этими переменными с другим кодом:
/// `async` block:
///
/// Multiple different `async` blocks can access the same local variable
/// so long as they're executed within the variable's scope
async fn blocks() {
let my_string = "foo".to_string();
let future_one = async {
// ...
println!("{my_string}");
};
let future_two = async {
// ...
println!("{my_string}");
};
// Run both futures to completion, printing "foo" twice:
let ((), ()) = futures::join!(future_one, future_two);
}
/// `async move` block:
///
/// Only one `async move` block can access the same captured variable, since
/// captures are moved into the `Future` generated by the `async move` block.
/// However, this allows the `Future` to outlive the original scope of the
/// variable:
fn move_block() -> impl Future<Output = ()> {
let my_string = "foo".to_string();
async move {
// ...
println!("{my_string}");
}
}
.await
в многопоточном исполнителе
Обратите внимание, что при использовании Future
в многопоточном исполнителе, Future
может перемещаться между потоками. Поэтому любые переменные, используемые в телах async
, должны иметь возможность перемещаться между потоками, так как любой .await
потенциально может привести к переключению на новый поток.
Это означает, что не безопасно использовать Rc
, &RefCell
или любые другие типы, не реализующие типаж Send
(включая ссылки на типы, которые не реализуют типаж Sync
).
(Предостережение: эти типы можно использовать, если они не находятся в области видимости во время вызова .await
.)
Точно так же не очень хорошая идея держать традиционную non-futures-aware блокировку через .await
, так как это может привести к блокировке пула потоков: одна задача может получить объект блокировки, вызвать .await
и передать управление исполнителю, разрешив другой задаче совершить попытку взять блокировку, что вызовет взаимную блокировку. Чтобы избежать этого, используйте Mutex
из futures::lock
, а не из std::sync
.