Какова цель async / await в Rust? - PullRequest
0 голосов
/ 16 октября 2018

На языке, подобном C #, с указанием этого кода (я специально не использую ключевое слово await):

async Task Foo()
{
    var task = LongRunningOperationAsync();

    // Some other non-related operation
    AnotherOperation();

    result = task.Result;
}

В первой строке длинная операция выполняется в другом потоке, иTask возвращается (это будущее).Затем вы можете выполнить другую операцию, которая будет выполняться параллельно первой, и в конце вы можете дождаться завершения операции.Я думаю, что это также поведение async / await в Python, JavaScript и т. Д.

С другой стороны, в Rust я прочитал в RFC , что:

Принципиальное отличие фьючерсов Rust от других языков заключается в том, что фьючерсы Rust ничего не делают, если не опрашиваются.Вся система построена вокруг этого: например, отмена отбрасывает будущее именно по этой причине.Напротив, в других языках вызов асинхронного fn раскручивает будущее, которое начинает выполняться немедленно.

Какова цель async / await в Rust в этой ситуации?Что касается других языков, эта запись удобна для запуска параллельных операций, но я не могу понять, как она работает в Rust, если вызов функции async ничего не запускает.

Ответы [ 3 ]

0 голосов
/ 16 октября 2018

Рассмотрим этот простой псевдо-JavaScript-код, который извлекает некоторые данные, обрабатывает их, извлекает еще некоторые данные на основе предыдущего шага, суммирует их и затем печатает результат:

getData(url)
   .then(response -> parseObjects(response.data))
   .then(data -> findAll(data, 'foo'))
   .then(foos -> getWikipediaPagesFor(foos))
   .then(sumPages)
   .then(sum -> console.log("sum is: ", sum));

В async/awaitформа, это:

async {
    let response = await getData(url);
    let objects = parseObjects(response.data);
    let foos = findAll(objects, 'foo');
    let pages = await getWikipediaPagesFor(foos);
    let sum = sumPages(pages);
    console.log("sum is: ", sum);
}

Он вводит много одноразовых переменных и, возможно, хуже, чем оригинальная версия с обещаниями.Так зачем беспокоиться?

Рассмотрим это изменение, когда переменные response и objects понадобятся позже в вычислении:

async {
    let response = await getData(url);
    let objects = parseObjects(response.data);
    let foos = findAll(objects, 'foo');
    let pages = await getWikipediaPagesFor(foos);
    let sum = sumPages(pages, objects.length);
    console.log("sum is: ", sum, " and status was: ", response.status);
}

И попробуйте переписать его в исходном видес обещаниями:

getData(url)
   .then(response -> Promise.resolve(parseObjects(response.data))
       .then(objects -> Promise.resolve(findAll(objects, 'foo'))
           .then(foos -> getWikipediaPagesFor(foos))
           .then(pages -> sumPages(pages, objects.length)))
       .then(sum -> console.log("sum is: ", sum, " and status was: ", response.status)));

Каждый раз, когда вам нужно вернуться к предыдущему результату, вам нужно вложить всю структуру на один уровень глубже.Это может быстро стать очень трудным для чтения и обслуживания, но версия async / await не страдает от этой проблемы.

0 голосов
/ 09 марта 2019

Цель async / await в Rust - предоставить инструментарий для параллелизма - такой же, как в C # и других языках.

В C # и JavaScript методы async запускаются немедленно,и они запланированы вне зависимости от того, * вы await результат или нет.В Python и Rust, когда вы вызываете метод async, ничего не происходит (даже не запланировано), пока вы не await.Но в любом случае это в основном один и тот же стиль программирования.

Я думаю, вы правы, что способность порождать другую задачу (которая выполняется одновременно и независимо от текущей задачи) является недостающей частью.Может быть, это будет добавлено.(Помните, что async Rust еще не закончен - дизайн все еще развивается.)


Что касается , почему Rust async не совсем так, как C #, хорошо, рассмотримразличия между этими двумя языками:

  • Rust препятствует глобальному изменяемому состоянию. В C # и JS каждый вызов метода async неявно добавляется в глобальную изменяемую очередь,Это побочный эффект для неявного контекста.Хорошо это или плохо, но это не стиль Rust.

  • Rust не является фреймворком. Имеет смысл, что C # обеспечивает цикл событий по умолчанию.Это также обеспечивает отличный сборщик мусора!Многое, что входит в стандартную комплектацию других языков, - это дополнительные библиотеки в Rust.

0 голосов
/ 16 октября 2018

Вы объединяете несколько понятий.

Параллельность - это не параллелизм , а 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 # слишком запутана, и вместо этого указали, что асинхронная функция всегда дает один раз перед выполнением любого кода.

В то время мне и другому в моей команде было поручено быть гвинеейСвинья, чтобы попробовать новый синтаксис и семантику в нашем менеджере пакетов.Основываясь на этом опыте, мы чувствовали, что асинхронные функции должны выполняться синхронно с первым ожиданием.Наши аргументы были в основном:

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

  2. Всегда сдавать означает, что определенные шаблоны не могут быть реализованы с использованием 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 будет такой:

  1. Нечто, реализующее 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
}
...