Вы объединяете несколько понятий.
Параллельность - это не параллелизм , а async
и await
являются инструментами для параллелизма , что иногда может означатьони также являются инструментами для параллелизма.
Кроме того, будущее опрашивается немедленно или нет, ортогонально выбранному синтаксису.
async
/ await
ключевые слова async
и await
существуют для того, чтобы сделать создание и взаимодействие с асинхронным кодом более легким для чтения и больше похожим на «обычный» синхронный код.Насколько мне известно, это верно для всех языков, в которых есть такие ключевые слова.
Более простой код
Это код, который создает будущее, которое добавляет два числа при опросе
до
fn long_running_operation(a: u8, b: u8) -> impl Future<Output = u8> {
struct Value(u8, u8);
impl Future for Value {
type Output = u8;
fn poll(self: Pin<&mut Self>, _lw: &LocalWaker) -> Poll<Self::Output> {
Poll::Ready(self.0 + self.1)
}
}
Value(a, b)
}
после
async fn long_running_operation(a: u8, b: u8) -> u8 {
a + b
}
Обратите внимание, что код "before" в основном является реализацией сегодняшняя poll_fn
функция
См. также Ответ Питера Холла о том, как отслеживать многие переменные можно сделать лучше.
Ссылки
Одна из потенциально удивительных вещей в async
/ await
заключается в том, что он допускает определенный шаблон, который был невозможен раньше: использование ссылок во фьючерсах.Вот некоторый код, который заполняет буфер значением асинхронно:
перед
use std::io;
fn fill_up<'a>(buf: &'a mut [u8]) -> impl Future<Output = io::Result<usize>> + 'a {
futures::future::lazy(move |_| {
for b in buf.iter_mut() { *b = 42 }
Ok(buf.len())
})
}
fn foo() -> impl Future<Output = Vec<u8>> {
let mut data = vec![0; 8];
fill_up(&mut data).map(|_| data)
}
Сбой компиляции:
error[E0597]: `data` does not live long enough
--> src/main.rs:33:17
|
33 | fill_up_old(&mut data).map(|_| data)
| ^^^^^^^^^ borrowed value does not live long enough
34 | }
| - `data` dropped here while still borrowed
|
= note: borrowed value must be valid for the static lifetime...
error[E0505]: cannot move out of `data` because it is borrowed
--> src/main.rs:33:32
|
33 | fill_up_old(&mut data).map(|_| data)
| --------- ^^^ ---- move occurs due to use in closure
| | |
| | move out of `data` occurs here
| borrow of `data` occurs here
|
= note: borrowed value must be valid for the static lifetime...
после
use std::io;
async fn fill_up(buf: &mut [u8]) -> io::Result<usize> {
for b in buf.iter_mut() { *b = 42 }
Ok(buf.len())
}
async fn foo() -> Vec<u8> {
let mut data = vec![0; 8];
fill_up(&mut data).await.expect("IO failed");
data
}
Это работает!
Вызов функции async
ничего не запускает
Реализация и проектирование Future
и вся система вокруг фьючерсов, с другой стороны, не связана с ключевыми словами async
и await
.Действительно, у Rust была процветающая асинхронная экосистема (например, с Tokio) до того, как когда-либо существовали ключевые слова async
/ await
.То же самое относится и к JavaScript.
Почему Future
не опрашивается сразу при создании?
Чтобы получить наиболее авторитетный ответ, прочитайте этот комментарий без лодок по запросу запроса RFC:
Принципиальное отличие фьючерсов Rust от других языков заключается в том, что фьючерсы Rust ничего не делают, если не опрашиваются.Вся система построена вокруг этого: например, отмена отбрасывает будущее именно по этой причине.Напротив, в других языках вызов асинхронного fn раскручивает будущее, которое начинает выполняться немедленно.
Суть в том, что async & await в Rust не являются по своей сути параллельными конструкциями.Если у вас есть программа, которая использует только асинхронные и await, а не примитивы параллелизма, код в вашей программе будет выполняться в определенном, статически известном, линейном порядке.Очевидно, что большинство программ используют некоторый параллелизм для планирования нескольких параллельных задач в цикле обработки событий, но это не обязательно.Это означает, что вы можете - тривиально - локально гарантировать упорядочение определенных событий, даже если между ними выполняется неблокирующий ввод-вывод, что вы хотите быть асинхронным с некоторым большим набором нелокальных событий (например, вы можете строго контролировать порядок событийвнутри обработчика запросов, будучи параллельным со многими другими обработчиками запросов, даже на двух сторонах точки ожидания).
Это свойство дает синтаксису Rust асинхронный / ожидающий вид локальных рассуждений и низкоуровневого управления, котороеделает Rust таким, какой он есть.Запуск до первой точки ожидания не нарушит это по своей сути - вы все равно будете знать, когда код будет выполнен, он просто будет выполняться в двух разных местах в зависимости от того, был ли он до или после ожидания.Тем не менее, я думаю, что решение, принятое другими языками для немедленного выполнения, во многом зависит от их систем, которые сразу же планируют задачу одновременно, когда вы вызываете асинхронный вызов fn (например, это впечатление основной проблемы, которую я получил из документа Dart 2.0)..
Некоторая часть истории Dart 2.0 покрыта этим обсуждением от muntiful :
Привет, я в команде Dart.Dyn's async / await был разработан главным образом Эриком Мейером, который также работал над async / await для C #.В C # async / await является синхронным с первым await.Дарт, Эрик и другие чувствовали, что модель C # слишком запутана, и вместо этого указали, что асинхронная функция всегда дает один раз перед выполнением любого кода.
В то время мне и другому в моей команде было поручено быть гвинеейСвинья, чтобы попробовать новый синтаксис и семантику в нашем менеджере пакетов.Основываясь на этом опыте, мы чувствовали, что асинхронные функции должны выполняться синхронно с первым ожиданием.Наши аргументы были в основном:
Всегда одна уступка влечет за собой снижение производительности без уважительной причины.В большинстве случаев это не имеет значения, но в некоторых это действительно имеет значение.Даже в тех случаях, когда вы можете с этим смириться, нужно перетаскивать немного воды повсюду.
Всегда сдавать означает, что определенные шаблоны не могут быть реализованы с использованием async / await.В частности, очень распространено иметь такой код (здесь псевдокод):
getThingFromNetwork():
if (downloadAlreadyInProgress):
return cachedFuture
cachedFuture = startDownload()
return cachedFuture
Другими словами, у вас есть асинхронная операция, которую вы можете вызывать несколько раз до ее завершения.Более поздние вызовы используют то же самое ранее созданное ожидающее будущее.Вы хотите убедиться, что не запускаете операцию несколько раз.Это означает, что вам нужно синхронно проверять кэш перед началом операции.
Если асинхронные функции являются асинхронными с самого начала, вышеуказанная функция не может использовать async / await.
Мы сослались на наше дело, но в конечном итоге разработчики языка зациклились на асинхронности сверху.Это было несколько лет назад.
Это оказалось неправильным вызовом.Затраты на производительность достаточно реальны, поэтому многие пользователи разработали мышление о том, что «асинхронные функции работают медленно», и начали избегать его использования даже в тех случаях, когда доступ к производительности был доступным.Хуже того, мы видим неприятные ошибки параллелизма, когда люди думают, что они могут выполнять некоторую синхронную работу на вершине функции, и с ужасом обнаруживают, что они создали условия гонки.В целом, кажется, что пользователи не предполагают, что асинхронная функция дает результат до выполнения какого-либо кода.
Итак, для Dart 2 мы сейчас принимаем очень болезненное критическое изменение, чтобы изменить асинхронные функции синхронно с первым ожиданием.и перенести весь наш существующий код через этот переход.Я рад, что мы вносим изменения, но я действительно хотел бы, чтобы мы сделали правильную вещь в первый день.
Я не знаю, накладывает ли модель владения и производительности в Rust различные ограничения на вас, когда выасинхронность с вершины действительно лучше, но по нашему опыту, синхронизация с первым ожиданием является лучшим компромиссом для Дарт.
ответы Крамерта (обратите внимание, что часть этого синтаксиса сейчас устарела):
Если вам нужен код для немедленного выполнения при вызове функции, а не позднее при опросе будущего, вы можете написать свою функцию следующим образом:
fn foo() -> impl Future<Item=Thing> {
println!("prints immediately");
async_block! {
println!("prints when the future is first polled");
await!(bar());
await!(baz())
}
}
Примеры кода
В этих примерах используется асинхронная поддержка в 1.37.0 ночью (2019-06-05) и корзине предварительного просмотра futures (0.3.0-alpha.16).
Буквальная транскрипция кода C #
#![feature(async_await)]
async fn long_running_operation(a: u8, b: u8) -> u8 {
println!("long_running_operation");
a + b
}
fn another_operation(c: u8, d: u8) -> u8 {
println!("another_operation");
c * d
}
async fn foo() -> u8 {
println!("foo");
let sum = long_running_operation(1, 2);
another_operation(3, 4);
sum.await
}
fn main() {
let task = foo();
futures::executor::block_on(async {
let v = task.await;
println!("Result: {}", v);
});
}
Если вы позвоните foo
, последовательность событий в Rust будет такой:
- Нечто, реализующее
Future<Output = u8>
, возвращается.
Вот и все.Никакой "фактической" работы еще не сделано.Если вы возьмете результат foo
и доведите его до завершения (опросив его, в данном случае через futures::executor::block_on
), то следующие шаги:
Нечто, реализующее Future<Output = u8>
, возвращается из вызова long_running_operation
(оно еще не начинает работать).
another_operation
работает синхронно.
синтаксис .await
вызывает запуск кода в long_running_operation
.Будущее foo
будет продолжать возвращаться "не готово" до тех пор, пока не будет выполнено вычисление.
Вывод будет:
foo
another_operation
long_running_operation
Result: 3
Обратите внимание, что нетпулы потоков здесь: все это делается в одном потоке.
async
блоков
Вы также можете использовать async
блоков:
use futures::{future, FutureExt};
fn long_running_operation(a: u8, b: u8) -> u8 {
println!("long_running_operation");
a + b
}
fn another_operation(c: u8, d: u8) -> u8 {
println!("another_operation");
c * d
}
async fn foo() -> u8 {
println!("foo");
let sum = async { long_running_operation(1, 2) };
let oth = async { another_operation(3, 4) };
let both = future::join(sum, oth).map(|(sum, _)| sum);
both.await
}
Здесь мы переносимсинхронный код в блоке async
, а затем дождитесь завершения обоих действий, прежде чем эта функция будет завершена.
Обратите внимание, что перенос синхронного кода, подобного этому, не хорошая идея для всего, чтона самом деле займет много времени;см. Каков наилучший подход для инкапсуляции блокирующего ввода-вывода в будущем rs? для получения дополнительной информации.
С пулом потоков
use futures::{executor::ThreadPool, future, task::SpawnExt, FutureExt};
async fn foo(pool: &mut ThreadPool) -> u8 {
println!("foo");
let sum = pool
.spawn_with_handle(async { long_running_operation(1, 2) })
.unwrap();
let oth = pool
.spawn_with_handle(async { another_operation(3, 4) })
.unwrap();
let both = future::join(sum, oth).map(|(sum, _)| sum);
both.await
}