Rust: Проблема отслеживания для async / await (RFC 2394)

Созданный на 8 мая 2018  ·  308Комментарии  ·  Источник: rust-lang/rust

Это проблема отслеживания для RFC 2394 (rust-lang / rfcs # 2394), который добавляет в язык синтаксис async и await.

Я буду возглавлять работу по реализации этого RFC, но буду признателен за наставничество, так как у меня относительно небольшой опыт работы с rustc.

СДЕЛАТЬ:

Нерешенные вопросы:

A-async-await A-generators AsyncAwait-Triaged B-RFC-approved C-tracking-issue T-lang

Самый полезный комментарий

О синтаксисе: мне бы очень хотелось, чтобы await простым ключевым словом. Например, давайте посмотрим на проблему из блога:

Мы не совсем уверены, какой синтаксис нам нужен для ключевого слова await. Если что-то является будущим результата - как и любое будущее ввода-вывода, - вы хотите иметь возможность ожидать его, а затем применять к нему оператор ? . Но порядок приоритета для включения этого может показаться удивительным - await io_future? будет await первым и ? вторым, несмотря на то, что ? лексически более жестко привязан, чем await.

Я согласен с этим, но брекеты - это зло. Я думаю, легче запомнить, что ? имеет более низкий приоритет, чем await и заканчивается этим:

let foo = await future?

Легче читать, легче рефакторинг. Я считаю, что это лучший подход.

let foo = await!(future)?

Позволяет лучше понять порядок, в котором выполняются операции, но, по-моему, он менее читабелен.

Я верю, что как только вы поймете, что await foo? await сначала выполняет await находится слева, а ? - справа. Так что по-прежнему достаточно логично сначала await и обрабатывать Result после него.


Если есть какие-либо разногласия, просьба их выразить, чтобы мы могли обсудить их. Я не понимаю, что означает «тихий голос против». Мы все желаем добра Rust.

Все 308 Комментарий

Обсуждение здесь, похоже, утихло, поэтому свяжем его здесь как часть синтаксического вопроса await : https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/ 7344

Реализация заблокирована на № 50307.

О синтаксисе: мне бы очень хотелось, чтобы await простым ключевым словом. Например, давайте посмотрим на проблему из блога:

Мы не совсем уверены, какой синтаксис нам нужен для ключевого слова await. Если что-то является будущим результата - как и любое будущее ввода-вывода, - вы хотите иметь возможность ожидать его, а затем применять к нему оператор ? . Но порядок приоритета для включения этого может показаться удивительным - await io_future? будет await первым и ? вторым, несмотря на то, что ? лексически более жестко привязан, чем await.

Я согласен с этим, но брекеты - это зло. Я думаю, легче запомнить, что ? имеет более низкий приоритет, чем await и заканчивается этим:

let foo = await future?

Легче читать, легче рефакторинг. Я считаю, что это лучший подход.

let foo = await!(future)?

Позволяет лучше понять порядок, в котором выполняются операции, но, по-моему, он менее читабелен.

Я верю, что как только вы поймете, что await foo? await сначала выполняет await находится слева, а ? - справа. Так что по-прежнему достаточно логично сначала await и обрабатывать Result после него.


Если есть какие-либо разногласия, просьба их выразить, чтобы мы могли обсудить их. Я не понимаю, что означает «тихий голос против». Мы все желаем добра Rust.

У меня неоднозначные взгляды на то, что await является ключевым словом @Pzixel. Хотя он, безусловно, имеет эстетическую привлекательность и, возможно, более последователен, учитывая, что async является ключевым словом, «раздувание ключевых слов» на любом языке вызывает серьезную озабоченность. Тем не менее, имеет ли смысл иметь async без await зрения функциональности? Если да, возможно, мы оставим все как есть. В противном случае я бы предпочел сделать await ключевым словом.

Я думаю, легче запомнить, что ? имеет более низкий приоритет, чем await и заканчивается им.

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

Это также помогает не во всех случаях, например, функция, возвращающая Result<impl Future, _> :

let foo = await (foo()?)?;

Проблема здесь не просто в том, «можете ли вы понять приоритет одного await + ? », но также и в том, «как выглядит объединение нескольких ожиданий в цепочку». Таким образом, даже если мы просто выберем приоритет, у нас все равно будет проблема await (await (await first()?).second()?).third()? .

Сводка опций синтаксиса await , некоторые из RFC, а остальные из потока RFC:

  • Требовать каких-либо разделителей: await { future }? или await(future)? (это шумно).
  • Просто выберите приоритет, чтобы await future? или (await future)? выполняли ожидаемое (оба эти значения кажутся удивительными).
  • Объедините два оператора во что-то вроде await? future (это необычно).
  • Сделайте постфикс await как-нибудь, например, future await? или future.await? (это беспрецедентно).
  • Используйте новую сигилу, например, ? , как в future@? (это «линейный шум»).
  • Не используйте вообще никакого синтаксиса, делая ожидание неявным (это затрудняет просмотр точек приостановки). Чтобы это работало, акт построения будущего также должен быть явным. Это тема внутренней ветки, на которую я ссылался выше .

Тем не менее, имеет ли смысл иметь async без await зрения функциональности?

@alexreg Есть . Так работает, например, Котлин. Это вариант «неявного ожидания».

@rpjohnst Интересно. Что ж, я обычно за то, чтобы оставить async и await как явные особенности языка, поскольку я думаю, что это больше в духе Rust, но тогда я не специалист по асинхронному программированию. ..

@alexreg async / await - действительно приятная функция, поскольку я работаю с ней изо дня в день на C # (это мой основной язык). @rpjohnst очень хорошо классифицировал все возможности. Я предпочитаю второй вариант, согласен с другими соображениями (шумно / необычно / ...). Я работал с кодом async / await последние 5 лет или около того, очень важно иметь такие ключевые слова flag.

@rpjohnst

Таким образом, даже если мы просто выберем приоритет, у нас все равно будет проблема с await (await (await first ()?). Second ()?). Third () ?.

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

let first = await first()?;
let second = await first.second()?;
let third = await second.third()?;

Так что я думаю, что это нормально, если язык не поощряет писать код таким образом, чтобы сделать основной случай проще и лучше.

Hero away future await? выглядит интересно, хотя и незнакомо, но я не вижу никаких логических контраргументов против этого.

В моей практике вы никогда не пишете два await в одной строке.

Но происходит ли это потому, что это плохая идея независимо от синтаксиса, или просто потому, что существующий синтаксис await C # делает его уродливым? Люди приводили аналогичные аргументы в отношении try!() (предшественника ? ).

Постфиксная и неявная версии гораздо менее уродливы:

first().await?.second().await?.third().await?
first()?.second()?.third()?

Но происходит ли это потому, что это плохая идея независимо от синтаксиса, или просто потому, что существующий синтаксис await в C # делает его уродливым?

Я думаю, что это плохая идея независимо от синтаксиса, потому что наличие одной строки на операцию async уже достаточно сложно для понимания и трудно отлаживать. Объединить их в одно утверждение, кажется, еще хуже.

Например, давайте посмотрим на реальный код (я взял один фрагмент из своего проекта):

[Fact]
public async Task Should_UpdateTrackableStatus()
{
    var web3 = TestHelper.GetWeb3();
    var factory = await SeasonFactory.DeployAsync(web3);
    var season = await factory.CreateSeasonAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
    var request = await season.GetOrCreateRequestAsync("123");

    var trackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, Request.TrackableStatuses.First(), "Trackable status");
    var nonTrackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, 0, "Nontrackable status");

    await request.UpdateStatusAsync(trackableStatus);
    await request.UpdateStatusAsync(nonTrackableStatus);

    var statuses = await request.GetStatusesAsync();

    Assert.Single(statuses);
    Assert.Equal(trackableStatus, statuses.Single());
}

Это показывает, что на практике не стоит связывать await s, даже если это позволяет синтаксис, потому что это станет совершенно нечитаемым. await просто усложняет запись и чтение одной строки, но я делаю считаю, что это не единственная причина, по которой это плохо.

Постфиксная и неявная версии гораздо менее уродливы

Возможность различать запуск задачи и ожидание задачи действительно важна. Например, я часто пишу такой код (опять же, отрывок из проекта):

public async Task<StatusUpdate[]> GetStatusesAsync()
{
    int statusUpdatesCount = await Contract.GetFunction("getStatusUpdatesCount").CallAsync<int>();
    var getStatusUpdate = Contract.GetFunction("getStatusUpdate");
    var tasks = Enumerable.Range(0, statusUpdatesCount).Select(async i =>
    {
        var statusUpdate = await getStatusUpdate.CallDeserializingToObjectAsync<StatusUpdateStruct>(i);
        return new StatusUpdate(XDateTime.UtcOffsetFromTicks(statusUpdate.UpdateDate), statusUpdate.StatusCode, statusUpdate.Note);
    });

    return await Task.WhenAll(tasks);
}

Здесь мы создаем N асинхронных запросов и ожидаем их. Мы не ждем на каждой итерации цикла, но сначала создаем массив асинхронных запросов, а затем ждем их всех сразу.

Я не знаю Котлина, так что, может быть, они как-нибудь решат это. Но я не понимаю, как это можно выразить, если «выполнение» и «ожидание» задачи - одно и то же.


Поэтому я думаю, что неявная версия невозможна даже в гораздо более неявных языках, таких как C #.
В Rust с его правилами, которые даже не позволяют вам неявно преобразовывать u8 в i32 это было бы намного более запутанным.

@Pzixel Да, второй вариант звучит как один из наиболее предпочтительных. Я тоже использовал async/await в C #, но не очень часто, так как я не программировал на C # уже несколько лет. Что касается приоритета, мне более естественно await (future?) .

@rpjohnst Мне нравится идея постфиксного оператора, но меня также беспокоит удобочитаемость и предположения, которые люди сделают - это может легко запутаться для члена struct именем await .

Возможность различать запуск задачи и ожидание задачи действительно важна.

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

Ваш пример будет выглядеть примерно так:

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Это то, что я имел в виду, говоря «чтобы это работало, акт построения будущего также должен быть явным». Это очень похоже на работу с потоками в коде синхронизации: вызов функции всегда ожидает ее завершения, прежде чем возобновить вызов, и есть отдельные инструменты для введения параллелизма. Например, замыкания и thread::spawn / join соответствуют асинхронным блокам, а join_all / select / и т. Д.

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

Я верю, что это так. Я не вижу здесь, какой поток будет в этой функции, где есть точки, в которых выполнение прерывается до завершения await. Я вижу только блок async который говорит: «Здравствуйте, где-то здесь есть асинхронные функции, попробуйте выяснить, какие из них, вы будете удивлены!».

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

var a = await fooAsync(); // awaiting first task
var b = barAsync(); //running second task
var c = await bazAsync(); // awaiting third task
if (c.IsSomeCondition && !b.Status = TaskStatus.RanToCompletion) // if some condition is true and b is still running
{
   var firstFinishedTask = await Task.Any(b, Task.Delay(5000)); // waiting for 5 more seconds;
   if (firstFinishedTask != b) // our task is timeouted
      throw new Exception(); // doing something
   // more logic here
}
else
{
   // more logic here
}

Rust всегда стремится обеспечить полный контроль над происходящим. await позволяет указать точки, в которых процесс продолжения. Это также позволяет вам unwrap значение в будущем. Если вы разрешаете неявное преобразование на стороне использования, это имеет несколько последствий:

  1. Прежде всего, вам нужно написать грязный код, чтобы просто имитировать такое поведение.
  2. Теперь RLS и IDE должны ожидать, что нашим значением будет либо Future<T> или awaited T . Это не проблема с ключевыми словами - они существуют, тогда результат T , иначе Future<T>
  3. Это затрудняет понимание кода. В вашем примере я не понимаю, почему он прерывает выполнение в строке get_status_updates , но не в get_status_update . Они очень похожи друг на друга. Так что либо он работает не так, как был в исходном коде, либо настолько сложен, что я его не вижу, даже если хорошо знаком с предметом. Обе альтернативы не делают этот вариант преимуществом.

Я не вижу здесь, какой поток будет в этой функции, где есть точки, в которых выполнение прерывается до завершения await.

Да, это то, что я имел в виду, говоря «из-за этого точки подвески труднее увидеть». Если вы читали ветку связанных внутренних компонентов, я приводил аргументы в пользу того, почему это не такая уж большая проблема. Вам не нужно писать новый код, вы просто помещаете аннотации в другое место ( async блоки вместо await ed выражений). В IDE нет проблем с определением типа (всегда T для вызовов функций и Future<Output=T> для async блоков).

Замечу также, что ваше понимание, вероятно, неверно вне зависимости от синтаксиса. Функции async Rust не запускают никакого кода, пока они не будут каким-либо образом ожидаются, поэтому ваша проверка b.Status != TaskStatus.RanToCompletion всегда будет успешной. Это также до смерти обсуждалось в ветке RFC, если вам интересно, почему это работает именно так.

В вашем примере я не понимаю, почему он прерывает выполнение в строке get_status_updates , но не в get_status_update . Они очень похожи друг на друга.

Это делает выполнение прерывания в обоих местах. Ключ в том, что блоки async не запускаются, пока они не ожидаются, потому что это верно для всех фьючерсов в Rust, как я описал выше. В моем примере get_statuses вызывает (и, таким образом, ожидает) get_status_updates , затем в цикле, который он создает (но не ожидает), count фьючерсы, затем он вызывает (и, таким образом, ожидает ) join_all , после чего эти фьючерсы одновременно вызывают (и, таким образом, ожидают) get_status_update .

Единственная разница с вашим примером заключается в том, когда именно фьючерсы начинают работать - в вашем, это во время цикла; у меня это во время join_all . Но это фундаментальная часть работы фьючерсов Rust, не имеющая ничего общего с неявным синтаксисом или даже с async / await .

Замечу также, что ваше понимание, вероятно, неверно вне зависимости от синтаксиса. Асинхронные функции Rust вообще не запускают код до тех пор, пока они не будут каким-либо образом ожидаются, поэтому ваша проверка b.Status! = TaskStatus.RanToCompletion всегда будет успешной.

Да, задачи C # выполняются синхронно до первой точки приостановки. Спасибо, что указали на это.
Однако на самом деле это не имеет значения, потому что я все еще должен иметь возможность запускать какую-то задачу в фоновом режиме, выполняя остальную часть метода, а затем проверять, завершена ли фоновая задача. Например, это могло быть

var a = await fooAsync(); // awaiting first task
var b = Task.Run(() => barAsync()); //running background task somehow
// the rest of the method is the same

У меня есть ваше представление о блоках async и, как я вижу, они такие же, но с большим количеством недостатков. В исходном предложении каждая асинхронная задача связана с await . С блоками async каждая задача будет соединена с блоком async в точке построения, поэтому мы находимся почти в той же ситуации, что и раньше (соотношение 1: 1), но даже немного хуже, потому что это кажется более неестественно и труднее для понимания, потому что поведение звонков становится контекстно-зависимым. С помощью await я вижу let a = foo() или let b = await foo() и я бы знал, что эта задача только что построена или построена и ожидается. Если я вижу let a = foo() с блоками async мне нужно посмотреть, есть ли какие-нибудь async выше, если я вас правильно понял, потому что в этом случае

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Here is where task *construction* becomes explicit, as an async block:
        task.push(async {
            // Again, simply *calling* get_status_update looks just like a sync call:
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }

    // And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
    join_all(&tasks[..])
}

Ждем сразу все задачи, пока здесь

pub async fn get_statuses() -> Vec<StatusUpdate> {
    // get_status_updates is also an `async fn`, but calling it works just like any other call:
    let count = get_status_updates();

    let mut tasks = vec![];
    for i in 0..count {
        // Isn't "just a construction" anymore
        task.push({
            let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
            StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
        });
    }
    tasks 
}

Мы выполняем их один за другим.

Таким образом, я не могу сказать, как именно ведет себя эта часть:

let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))

Без дополнительного контекста.

А с вложенными блоками все становится еще страннее. Не говоря уже об инструментах и ​​т. Д.

поведение callite становится зависимым от контекста

Это уже верно для обычного кода синхронизации и замыканий. Например:

// Construct a closure, delaying `do_something_synchronous()`:
task.push(|| {
    let data = do_something_synchronous();
    StatusUpdate { data }
});

против

// Execute a block, immediately running `do_something_synchronous()`:
task.push({
    let data = do_something_synchronous();
    StatusUpdate { data }
});

Еще одна вещь, которую вы должны отметить из полного предложения неявного ожидания, заключается в том, что вы не можете вызывать async fn s из контекстов, отличных от async . Это означает, что синтаксис вызова функции some_function(arg1, arg2, etc) всегда выполняет тело some_function до завершения до того, как вызывающий абонент продолжит выполнение, независимо от того, является ли some_function async . Таким образом, вход в контекст async всегда явно отмечен, а синтаксис вызова функции на самом деле более согласован.

Что касается синтаксиса ожидания: как насчет макроса с синтаксисом метода? Я не могу найти настоящего RFC, разрешающего это, но я нашел несколько обсуждений ( 1 , 2 ) на Reddit, так что идея не беспрецедентна. Это позволит await работать в позиции постфикса, не делая его ключевым словом / вводя новый синтаксис только для этой функции.

// Postfix await-as-a-keyword. Looks as if we were accessing a Result<_, _> field,
// unless await is syntax-highlighted
first().await?.second().await?.third().await?
// Macro with method syntax. A few more symbols, but clearly a macro invocation that
// can affect control flow
first().await!()?.second().await!()?.third().await!()?

Есть библиотека из мира Scala, которая упрощает композиции монад: http://monadless.io

Может быть, какие-то идеи заинтересуют Rust.

цитата из документов:

Большинство основных языков поддерживают асинхронное программирование с использованием идиомы async / await или реализуют ее (например, F #, C # / VB, Javascript, Python, Swift). Несмотря на свою полезность, async / await обычно привязан к определенной монаде, которая представляет асинхронные вычисления (Task, Future и т. Д.).

Эта библиотека реализует решение, подобное async / await, но обобщенное для любого типа монады. Это обобщение является основным фактором, учитывая, что некоторые кодовые базы используют другие монады, такие как Task, в дополнение к Future для асинхронных вычислений.

Для монады M в обобщении используется концепция подъема регулярных значений в монаду ( T => M[T] ) и отмены подъема значений из экземпляра монады ( M[T] => T ). > Пример использования:

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Обратите внимание, что lift соответствует async, а unlift - await.

Это уже верно для обычного кода синхронизации и замыканий. Например:

Я вижу здесь несколько отличий:

  1. Лямбда-контекст неизбежен, но не для await . С await у нас нет контекста, с async он должен быть. Первый выигрывает, потому что он предоставляет те же функции, но требует меньшего знания кода.
  2. Лямбды обычно короткие, максимум несколько строк, поэтому мы видим все тело сразу, и простые. async функции могут быть довольно большими (такими же большими, как обычные функции) и сложными.
  3. Лямбды редко бывают вложенными (за исключением вызовов then , но для этого предлагается await ), блоки async вкладываются часто.

Еще одна вещь, которую вы должны отметить из предложения полного неявного ожидания, заключается в том, что вы не можете вызывать async fns из неасинхронных контекстов.

Хм, я этого не заметил. Звучит не очень хорошо, потому что в моей практике вы часто хотите запускать асинхронный режим из неасинхронного контекста. В C # async - это просто ключевое слово, которое позволяет компилятору переписать тело функции, оно никоим образом не влияет на интерфейс функции, поэтому async Task<Foo> и Task<Foo> полностью взаимозаменяемы, и это отделяет реализацию от API.

Иногда вы можете захотеть заблокировать задачу async , например, когда вы хотите вызвать какой-либо сетевой API из main . Вы должны заблокировать (иначе вы вернетесь в ОС и программа завершится), но вы должны выполнить асинхронный HTTP-запрос. Я не уверен, какое решение может быть здесь, кроме взлома main чтобы он был асинхронным, а также с Result основным типом возврата, если вы не можете вызвать его из неасинхронного основного .

Еще одно соображение в пользу текущего await - это то, как он работает на другом популярном языке (как отмечает @fdietze ). Это упрощает переход с другого языка, такого как C # / TypeScript / JS / Python, и, таким образом, является лучшим подходом с точки зрения привлечения новых людей.

Я вижу здесь несколько отличий

Вы также должны понимать, что в основном RFC уже есть блоки async с той же семантикой, что и неявная версия.

Звучит не очень хорошо, потому что в моей практике вы часто хотите запускать асинхронный режим из неасинхронного контекста.

Это не проблема. Вы можете использовать async блоки в непредставленных async контексты (это хорошо , потому что они просто оценить к F: Future , как всегда), и вы все еще можете икру или блок на фьючерсы используя точно такой же API, как и раньше.

Вы просто не можете вызвать async fn s, а вместо этого оберните вызов к ним в блок async как вы это делаете независимо от контекста, в котором вы находитесь, если вы хотите F: Future из этого.

async - это просто ключевое слово, которое позволяет компилятору переписывать тело функции, это никак не влияет на интерфейс функции

Да, это законная разница между предложениями. Это тоже было прикрыто внутренней нитью. Возможно, наличие разных интерфейсов для этих двух полезно, потому что это показывает вам, что версия async fn не будет запускать какой-либо код как часть конструкции, в то время как версия -> impl Future может, например, инициировать запрос перед тем, как дать вам а F: Future . Это также делает async fn s более согласованным с обычным fn s, поскольку вызов чего-то, объявленного как -> T , всегда будет давать вам T , независимо от того, async .

(Вы должны также отметить , что в Русте есть еще довольно скачок между async fn и Future -returning версии, как описано в RFC. async fn версия не упоминается Future любом месте его подписи; а ручная версия требует impl Trait , что влечет за собой некоторые проблемы, связанные с продолжительностью жизни. Фактически, это часть мотивации для async fn для начала.)

Это упрощает переход с другого языка, такого как C # / TypeScript / JS / Python.

Это преимущество только для буквального синтаксиса await future , который сам по себе довольно проблематичен в Rust. Все остальное, что у нас может получиться, также не соответствует этим языкам, в то время как неявное ожидание имеет, по крайней мере, а) сходство с Kotlin и б) сходство с синхронным, основанным на потоках кодом.

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

Я бы сказал, что у двух разных интерфейсов есть некоторые преимущества, потому что наличие зависимости API от деталей реализации мне не нравится. Например, вы пишете контракт, который просто делегирует вызов внутреннему будущему.

fn foo(&self) -> Future<T> {
   self.myService.foo()
}

А потом вы просто хотите добавить логирование

async fn foo(&self) -> T {
   let result = await self.myService.foo();
   self.logger.log("foo executed with result {}.", result);
   result
}

И это становится переломным моментом. Эй?

Это преимущество только для синтаксиса literal await future, который сам по себе довольно проблематичен в Rust. Все остальное, что у нас может получиться, также не соответствует этим языкам, в то время как неявное ожидание имеет, по крайней мере, а) сходство с Kotlin и б) сходство с синхронным, основанным на потоках кодом.

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

Вы также должны отметить, что в Rust все еще существует значительный скачок между async fn и версией, возвращаемой в будущем, как описано в RFC.

Я это знаю, и меня это очень беспокоит.

И это становится переломным моментом.

Вы можете обойти это, вернув блок async . В рамках неявного предложения ожидания ваш пример выглядит так:

fn foo(&self) -> impl Future<Output = T> { // Note: you never could return `Future<T>`...
    async { self.my_service.foo() } // ...and under the proposal you couldn't call `foo` outside of `async` either.
}

И с логированием:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        result
    }
}

Более серьезная проблема с наличием этого различия возникает во время перехода экосистемы от ручных будущих реализаций и комбинаторов (единственный способ сегодня) к async / await. Но даже в этом случае предложение позволяет сохранить старый интерфейс и предоставить вместе с ним новый асинхронный. Например, C # полон этого шаблона.

Что ж, звучит разумно.

Однако я верю, что такая неявность (мы не видим, является ли foo() здесь функцией async или sync) приводит к тем же проблемам, которые возникают в протоколах, таких как COM +, и является причиной реализации WCF в прежнем виде. . У людей возникали проблемы, когда асинхронные удаленные запросы выглядели как вызовы простых методов.

Этот код выглядит отлично, за исключением того, что я не вижу, есть ли какой-либо запрос, асинхронный или синхронный. Я считаю, что это важная информация. Например:

fn foo(&self) -> impl Future<Output = T> {
    async {
        let result = self.my_service.foo();
        self.logger.log("foo executed with result {}.", result);
        let bars: Vec<Bar> = Vec::new();
        for i in 0..100 {
           bars.push(self.my_other_service.bar(i, result));
        }
        result
    }
}

Очень важно знать, является ли bar синхронизирующей или асинхронной функцией. Я часто вижу await в цикле как маркер того, что этот код необходимо изменить, чтобы добиться лучших результатов при загрузке и производительности. Это код, который я просмотрел вчера (код неоптимальный, но это одна из итераций обзора):

image

Как видите, я легко заметил, что здесь есть цикл ожидания, и попросил его изменить. Когда изменение было зафиксировано, мы получили ускорение загрузки страницы в 3 раза. Без await я мог бы легко не заметить это плохое поведение.

Признаюсь, я не использовал Kotlin, но в прошлый раз, когда я смотрел на этот язык, мне показалось, что это в основном вариант Java с меньшим синтаксисом, вплоть до того момента, когда было легко механически перевести один в другой. Я также могу представить, почему он понравится в мире Java (который, как правило, немного перегружен синтаксисом), и я знаю, что в последнее время он стал популярнее, в частности, из-за того, что он не является Java (ситуация Oracle против Google ).

Однако, если мы решим принять во внимание популярность и знакомство, мы можем захотеть взглянуть на то, что делает JavaScript, что также является явным await .

Тем не менее, await был введен в основные языки с помощью C #, который, возможно, является одним из языков, удобство использования которого считалось чрезвычайно важным . В C # асинхронные вызовы обозначаются не только ключевым словом await , но также суффиксом Async вызовов методов. Другая языковая функция, которая больше всего похожа на await , yield return , также явно видна в коде.

Это почему? Я считаю, что генераторы и асинхронные вызовы - слишком мощные конструкции, чтобы позволить им пройти незамеченными в коде. Существует иерархия операторов потока управления:

  • последовательное выполнение операторов (неявное)
  • вызовы функций / методов (вполне очевидно, сравните, например, с Pascal где на месте вызова нет разницы между нулевой функцией и переменной)
  • goto (хорошо, это не строгая иерархия)
  • генераторы ( yield return обычно выделяются)
  • await + Async суффикс

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

Конечно, в других языках использовались другие подходы. Продолжение схемы (например, в call/cc , что не слишком отличается от await ) или макросы не имеют синтаксиса, чтобы показать, что вы вызываете. Что касается макросов, Rust упростил их просмотр.

Поэтому я бы сказал, что меньший синтаксис сам по себе нежелателен (для этого есть такие языки, как APL или Perl), и этот синтаксис не должен быть просто шаблонным и играет важную роль в удобочитаемости.

Также существует параллельный аргумент (извините, я не могу вспомнить источник, но он мог исходить от кого-то из языковой команды), что людям удобнее использовать шумный синтаксис для новых функций, когда они являются новыми, но тогда их устраивает менее подробный, как только они станут широко использоваться.


Что касается вопроса await!(foo)? против await foo? , я нахожусь в бывшем лагере. Вы можете усвоить практически любой синтаксис, однако мы слишком привыкли принимать сигналы от пробелов и близости. С await foo? есть вероятность, что кто-то будет повторно догадываться о приоритете двух операторов, в то время как фигурные скобки проясняют, что происходит. Сохранение трех персонажей того не стоит. А что касается практики связывания await! s, хотя это может быть популярной идиомой в некоторых языках, я считаю, что у нее слишком много недостатков, таких как плохая читаемость и взаимодействие с отладчиками, чтобы ее стоило оптимизировать.

Сохранение трех персонажей того не стоит.

По моему анекдотическому опыту, лишние символы (например, более длинные имена) не являются большой проблемой, но лишние токены могут действительно раздражать. С точки зрения аналогии с процессором, длинное имя - это прямой код с хорошей локализацией - я могу просто набрать его из мышечной памяти - в то время как такое же количество символов, когда оно включает несколько токенов (например, пунктуацию), является разветвленным и полным пропусков кеша.

(Я полностью согласен с тем, что await foo? было бы крайне неочевидным, и нам следует избегать этого, и что необходимость вводить больше токенов будет намного предпочтительнее; мое наблюдение состоит только в том, что не все символы созданы равными.)


@rpjohnst Я думаю, ваше альтернативное предложение могло бы получить немного лучший прием, если бы оно было представлено как "явный асинхронный", а не "неявное ожидание" :-)

Крайне важно знать, является ли bar синхронизирующей или асинхронной функцией.

Я не уверен, что это действительно чем-то отличается от знания того, является ли какая-то функция дешевой или дорогой, выполняет ли она ввод-вывод или нет, или касается ли она какого-либо глобального состояния или нет. (Это также относится к иерархии @lnicola - если асинхронные вызовы выполняются до завершения так же, как вызовы синхронизации, тогда они действительно ничем не отличаются с точки зрения мощности!)

Например, тот факт, что вызов был в цикле, так же, если не больше, важен, чем тот факт, что он был асинхронным. А в Rust, где распараллеливание намного проще осуществить, вы могли бы с таким же успехом предложить переключить дорогостоящие синхронные циклы на итераторы Rayon!

Поэтому я не думаю, что требование await на самом деле так важно для выявления этих оптимизаций. Циклы уже всегда являются хорошим местом для поиска оптимизации, а async fn s уже являются хорошим индикатором того, что вы можете получить дешевый параллелизм ввода-вывода. Если вы обнаружите, что упускаете эти возможности, вы даже можете написать Clippy lint для «асинхронного вызова в цикле», который вы запускаете время от времени. Было бы здорово иметь аналогичный линт и для синхронного кода!

Мотивация для «явной асинхронности» заключается не просто в «меньшем синтаксисе», как предполагает @lnicola . Это сделано для того, чтобы поведение синтаксиса вызова функций было более согласованным, чтобы foo() всегда выполнял тело foo до завершения. В соответствии с этим предложением отсутствие аннотации просто дает вам менее параллельный код, как практически весь код уже ведет себя. В «явном ожидании» исключение аннотации приводит к случайному параллелизму или, по крайней мере, случайному чередованию, что проблематично .

Я думаю, ваше альтернативное предложение могло бы быть немного лучше воспринято, если бы оно было представлено как «явный асинхронный», а не «неявное ожидание» :-)

Поток называется «явное построение будущего, неявное ожидание», но, похоже, последнее имя прижилось. :П

Я не уверен, что это действительно чем-то отличается от знания того, является ли какая-то функция дешевой или дорогой, выполняет ли она ввод-вывод или нет, или касается ли она какого-либо глобального состояния или нет. (Это также относится к иерархии @lnicola - если асинхронные вызовы выполняются до завершения так же, как вызовы синхронизации, тогда они действительно ничем не отличаются с точки зрения мощности!)

Я думаю, это так же важно, как знать, что функция меняет какое-то состояние, и у нас уже есть ключевое слово mut на обеих сторонах вызова и вызывающей стороне.

Мотивация для «явной асинхронности» заключается не просто в «меньшем синтаксисе», как предполагает @lnicola . Это сделано для того, чтобы поведение синтаксиса вызова функции было более согласованным, чтобы foo () всегда выполняла тело foo до завершения.

С одной стороны, это хорошее соображение. С другой стороны, вы можете легко разделить будущее создание и будущий запуск. Я имею в виду, что если foo возвращает вам некоторую абстракцию, которая позволяет вам затем вызвать run и получить какой-то результат, это не сделает foo бесполезным мусором, который ничего не делает, он делает очень полезная вещь: он создает какой-то объект, который вы можете вызывать позже методами. Это не делает ничего другого. Вызываемый нами метод foo - это просто черный ящик, и мы видим его подпись Future<Output=T> и он фактически возвращает будущее. Поэтому мы явно await it, когда захотим.

Поток называется «явное построение будущего, неявное ожидание», но, похоже, последнее имя прижилось. :П

Я лично считаю, что лучшая альтернатива - "явное async явное ожидание" :)


PS

Сегодня меня также поразила мысль: вы пробовали общаться с C # LDM? Например, такие парни, как @HaloFour , @gafter или @CyrusNajmabadi . Может быть, стоит спросить их, почему они взяли такой синтаксис. Я бы предложил спросить и у ребят из других языков, но я их просто не знаю :) Я уверен, что у них было много споров о существующем синтаксисе, и они уже могли много его обсуждать, и у них могут быть некоторые полезные идеи.

Это не означает, что в Rust должен быть такой синтаксис, как в C #, но он просто позволяет принимать более взвешенные решения.

Я лично считаю, что лучшая альтернатива - "явное async явное ожидание" :)

Но главное предложение - не «явная асинхронность» - поэтому я выбрал это название. Это « неявная асинхронность», потому что вы не можете сразу сказать, где именно асинхронность вводится. Любой неаннотированный вызов функции может строить будущее, не ожидая его, даже если Future нигде не фигурирует в своей сигнатуре.

Для чего это стоит, внутренностей нить делает включать «явное асинхронной явное ОЖИДАНИЕ» альтернативу, потому что это будущее совместим с любой основной альтернативой. (См. Последний раздел первого сообщения.)

вы пробовали общаться с C # LDM?

Это сделал автор основного RFC. Насколько я помню, главным результатом было решение не включать Future в подпись async fn s. В C # вы можете заменить Task другими типами, чтобы иметь некоторый контроль над тем, как выполняется функция. Но в Rust у нас нет (и не будет) такого механизма - все фьючерсы будут проходить через одну черту, поэтому нет необходимости каждый раз прописывать эту черту.

Мы также общались с разработчиками языка Dart, и это было значительной частью моей мотивации для написания предложения «явной асинхронности». У Dart 1 была проблема, потому что функции не запускались до своего первого ожидания при вызове (не совсем так, как работает Rust, но похоже), и это вызвало такую ​​огромную путаницу, что в Dart 2 они изменились, поэтому функции запускаются до своего первого жду, когда позвонят. Rust не может этого сделать по другим причинам, но он может запускать всю функцию при вызове, что также позволяет избежать этой путаницы.

Мы также общались с разработчиками языка Dart, и это было значительной частью моей мотивации для написания предложения «явной асинхронности». У Dart 1 была проблема, потому что функции не запускались до своего первого ожидания при вызове (не совсем так, как работает Rust, но похоже), и это вызвало такую ​​огромную путаницу, что в Dart 2 они изменились, поэтому функции запускаются до своего первого жду, когда позвонят. Rust не может этого сделать по другим причинам, но он может запускать всю функцию при вызове, что также позволяет избежать этой путаницы.

Отличный опыт, я не знал об этом. Приятно слышать, что вы проделали такую ​​огромную работу. Молодцы 👍

Сегодня меня также поразила мысль: вы пробовали общаться с C # LDM? Например, такие парни, как @HaloFour , @gafter или @CyrusNajmabadi . Может быть, стоит спросить их, почему они взяли такой синтаксис.

Я рад предоставить любую интересующую вас информацию. Однако я лишь бегло просмотрел ее. Можно ли было бы вкратце сформулировать какие-либо конкретные вопросы, которые у вас есть сейчас?

Что касается синтаксиса await (это может быть совершенно глупо, не стесняйтесь кричать на меня; я нуб асинхронного программирования и понятия не имею, о чем говорю):

Вместо использования слова «ожидание», нельзя ли ввести символ / оператор, аналогичный ? . Например, это может быть # или @ или что-то еще, что в настоящее время не используется.

Например, если бы это был постфиксный оператор:

let stuff = func()#?;
let chain = blah1()?.blah2()#.blah3()#?;

Он очень краток и естественно читается слева направо: сначала ждите ( # ), затем обрабатывайте ошибки ( ? ). У него нет проблемы с ключевым словом postfix await, где .await выглядит как член структуры. # явно оператор.

Я не уверен, что postfix - подходящее место для него, но мне так казалось из-за приоритета. В качестве префикса:

let stuff = #func()?;

Или, черт возьми, даже:

let stuff = func#()?; // :-D :-D

Обсуждалось ли это когда-нибудь?

(Я понимаю, что этот вид начинает приближаться к синтаксису "случайного набора символов клавиатуры", которым Perl печально известен ... :-D)

@rayvector https://github.com/rust-lang/rust/issues/50547#issuecomment -388108875, пятая альтернатива.

@CyrusNajmabadi, спасибо, что пришли. Главный вопрос заключается в том, какой вариант из перечисленных, по вашему мнению, лучше подходит для текущего языка Rust, как он есть, или, может быть, есть какая-то другая альтернатива? Эта тема не очень длинная, поэтому вы можете легко ее быстро прокрутить сверху вниз. Главный вопрос: следует ли Rust следовать текущему пути C # / TS / ... await или, может быть, он должен реализовать свой собственный. Является ли текущий синтаксис своего рода «устаревшим», который вы хотели бы каким-либо образом изменить, или он лучше всего подходит для C #, а также для новых языков?

Основное соображение против синтаксиса C # - приоритет оператора await foo? должен сначала ждать, а затем оценивать оператор ? а также различие, которое, в отличие от C #, не выполняется в потоке вызывающего абонента до первого await , но не запускается вообще, так как текущий фрагмент кода не запускает проверки на отрицательность до тех пор, пока GetEnumerator будет вызван в первый раз:

IEnumerable<int> GetInts(int n)
{
   if (n < 0)
      throw new InvalidArgumentException(nameof(n));
   for (int i = 0; i <= n; i++)
      yield return i;
}

Более подробно в моем первом комментарии и последующем обсуждении.

@Pzixel О, наверное, я пропустил это, когда просматривал эту ветку ранее ...

В любом случае, я не видел особого обсуждения этого вопроса, кроме этого краткого упоминания.

Есть ли веские аргументы за / против?

@rayvector я немного поспорил здесь в пользу более подробного синтаксис. Одна из причин - та, которую вы упомянули:

синтаксис "случайного набора символов клавиатуры", которым печально известен Perl

Чтобы прояснить, я не думаю, что await!(f)? действительно работает над окончательным синтаксисом, он был выбран специально потому, что это надежный способ не делать никаких конкретных решений. Вот синтаксисы (включая оператор ? ), которые, как мне кажется, все еще «работают»:

  • await f?
  • await? f
  • await { f }?
  • await(f)?
  • (await f)?
  • f.await?

Или, возможно, какая-то их комбинация. Дело в том, что некоторые из них действительно содержат фигурные скобки, чтобы было понятнее о приоритете, и здесь есть много вариантов, но намерение состоит в том, что await будет оператором ключевого слова, а не макросом, в окончательной версии ( за исключением некоторых серьезных изменений, подобных тому, что предлагает rpjohnst).

Я голосую либо за простой постфиксный оператор await (например, ~ ), либо за ключевое слово без скобок и с наивысшим приоритетом.

Я читал эту ветку и хотел бы предложить следующее:

  • await f? сначала вычисляет оператор ? , а затем ожидает результирующего future.
  • (await f)? сначала ожидает будущее, а затем сравнивает оператор ? с результатом (из-за обычного приоритета оператора Rust)
  • await? f доступен как синтаксический сахар для `(await f) ?. Я считаю, что «будущее, возвращающее результат» будет очень распространенным случаем, поэтому специальный синтаксис имеет большой смысл.

Я согласен с другими комментаторами, что await должно быть явным. Сделать это в JavaScript довольно безболезненно, и я очень ценю ясность и читаемость кода Rust, и мне кажется, что создание неявного async разрушит это для асинхронного кода.

Мне пришло в голову, что «неявный асинхронный блок» должен быть реализован как proc_macro, который просто вставляет ключевое слово await перед любым будущим.

Главный вопрос - какой вариант из перечисленных, по вашему мнению, лучше подходит для текущего языка Rust, как он есть,

Спросить разработчика C #, что лучше всего подходит для языка rust ... интересно :)

Я не чувствую себя достаточно квалифицированным, чтобы принимать такое решение. Я люблю ржавчину и балуюсь ею. Но это не тот язык, который я использую изо дня в день. Я также не укоренил это глубоко в моей психике. Таким образом, я не думаю, что у меня есть право делать какие-либо заявления о том, какие варианты здесь подходят для этого языка. Хотите спросить меня о Go / TypeScript / C # / VB / C ++. Конечно, мне было бы намного комфортнее. Но ржавчина - это слишком много для меня, чтобы чувствовать себя комфортно с такими мыслями.

Основное возражение против синтаксиса C # - приоритет оператора await foo?

Я чувствую, что могу это прокомментировать. Мы много думали о приоритете с помощью await и опробовали множество форм, прежде чем установить ту форму, которую мы хотели. Одна из основных вещей, которые мы обнаружили, заключалась в том, что для нас и для клиентов (внутренних и внешних), которые хотели использовать эту функцию, люди редко действительно хотели «связать» что-либо помимо своего асинхронного вызова. Другими словами, люди, похоже, сильно тяготели к тому, чтобы «ожидание» было самой важной частью любого полного выражения, и, таким образом, оно было ближе к вершине. Примечание: под «полным выражением» я подразумеваю такие вещи, как выражение, которое вы получаете в верхней части выражения-выражения, или выражение справа от назначения верхнего уровня, или выражение, которое вы передаете как «аргумент» чему-либо.

Тенденция к желанию «продолжить» с «await» внутри выражения была редкой. Иногда мы видим такие вещи, как (await expr).M() , но они кажутся менее распространенными и менее желательными, чем количество людей, выполняющих await expr.M() .

По этой же причине мы не использовали «неявную» форму для слова «await». На практике это было то, о чем люди хотели очень четко подумать, и что они хотели, чтобы они занимали центральное место в своем коде, чтобы они могли обратить на это внимание. Интересно, что даже спустя годы эта тенденция сохранилась. т.е. иногда спустя много лет мы сожалеем о том, что что-то излишне многословно. Некоторые функции хороши в этом отношении на раннем этапе, но когда люди освоятся с ними, им лучше подойдет что-то более компактное. В случае с await этого не произошло. Людям по-прежнему нравится тяжелый характер этого ключевого слова и выбранный нами приоритет.

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

-

а также разница в том, что, в отличие от C #, выполнение в вызывающем потоке не выполняется до первого ожидания await, но не запускается вообще, текущий фрагмент кода не запускает проверки на отрицательность до тех пор, пока GetEnumerator не будет вызван в первый раз:

ИМО, то, как мы использовали счетчиков, было в некоторой степени ошибкой и привело к множеству путаницы на протяжении многих лет. Это было особенно плохо из-за склонности к написанию большого количества кода следующим образом:

`` С #
void SomeEnumerator (X аргументы)
{
// Проверяем аргументы, выполняем синхронную работу.
return SomeEnumeratorImpl (аргументы);
}

void SomeEnumeratorImpl (X аргументы)
{
// ...
урожай
// ...
}

People have to write this *all the time* because of the unexpected behavior that the iterator pattern has.  I think we were worried about expensive work happening initially.  However, in practice, that doesn't seem to happen, and people def think about the work as happening when the call happens, and the yields themselves happening when you actually finally start streaming the elements.

Linq (which is the poster child for this feature) needs to do this *everywhere*, this highly diminishing this choice.

For ```await``` i think things are *much* better.  We use 'async/await' a ton ourselves, and i don't think i've ever once said "man... i wish that it wasn't running the code synchronously up to the first 'await'".  It simply makes sense given what the feature is.  The feature is literally "run the code up to await points, then 'yield', then resume once the work you're yielding on completes".  it would be super weird to not have these semantics to me since it is precisely the 'awaits' that are dictating flow, so why would anything be different prior to hitting the first await.

Also... how do things then work if you have something like this:

```c#
async Task FooAsync()
{
    if (cond)
    {
        // only await in method
        await ...
    }
} 

Вы можете полностью вызвать этот метод и никогда не нажимать на ожидание. если «выполнение не выполняется в вызывающем потоке до первого ожидания», что на самом деле здесь происходит?

Ждите? f доступен как синтаксический сахар для `(await f) ?. Я считаю, что «будущее, возвращающее результат» будет очень распространенным случаем, поэтому специальный синтаксис имеет большой смысл.

Это больше всего меня резонирует. Это позволяет «ожидание» быть самой важной концепцией, но также позволяет легко обрабатывать типы результатов.

Одна вещь, которую мы знаем из C #, заключается в том, что интуиция людей относительно приоритета привязана к пробелам. Итак, если у вас есть "ждать х?" тогда сразу же кажется, что await имеет меньший приоритет, чем ? потому что ? упирается в выражение. Если бы вышеперечисленное на самом деле было проанализировано как (await x)? это было бы неожиданностью для нашей аудитории.

Разбирать его как await (x?) было бы наиболее естественно только из синтаксиса и соответствовало бы потребности в получении `` результата '' будущего / задачи обратно и желанию `` ждать '', что, если вы действительно получили значение . Если это затем само вернуло Result, будет уместно объединить его с await, чтобы сигнализировать о том, что это произойдет потом. поэтому await? x? каждый ? плотно привязывается к той части кода, к которой он наиболее естественно относится. Первый ? относится к await (и, в частности, к его результату), а второй относится к x .

если «выполнение не выполняется в вызывающем потоке до первого ожидания», что на самом деле здесь происходит?

Ничего не происходит до тех пор, пока вызывающий объект не ожидает возвращаемого значения FooAsync , после чего тело FooAsync выполняется до тех пор, пока либо не появится await либо оно не вернется.

Это работает так, потому что Rust Future s управляются опросами, распределяются по стеку и остаются неподвижными после первого вызова poll . Вызывающий должен иметь возможность переместить их на место - в куче для верхнего уровня Future s, или же по значению внутри родительского Future , часто в «кадре стека». вызова async fn перед выполнением любого кода.

Это означает, что мы застряли либо с а) семантикой, подобной генератору C #, где код не запускается при вызове, или б) семантикой, подобной сопрограмме Kotlin, где вызов функции также немедленно и неявно ожидает ее (с подобным замыканию async { .. } блоки на случай, когда вам действительно нужно одновременное выполнение).

Я предпочитаю последнее, потому что оно позволяет избежать проблемы, о которой вы упоминаете, с генераторами C #, а также полностью избегает вопроса о приоритете операторов.

@CyrusNajmabadi В Rust Future обычно не работает, пока не будет порожден как Task (он намного больше похож на F # Async ):

let bar = foo();

В этом случае foo() возвращает Future , но, вероятно, на самом деле ничего не делает . Вы должны создать его вручную (что также похоже на F # Async ):

tokio::run(bar);

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

Очевидно, что в C # ситуация иная, потому что в C #, когда вы вызываете foo() сразу запускается Task , поэтому в C # имеет смысл запускать код до первого await .

Также ... как все работает, если у вас есть что-то вроде этого [...] Вы можете полностью вызвать этот метод и никогда не нажимать await. если «выполнение не выполняется в вызывающем потоке до первого ожидания», что на самом деле здесь происходит?

Если вы вызываете FooAsync() он ничего не делает, код не запускается. Затем, когда вы его создадите, он запустит код синхронно, await никогда не запустится, и поэтому он сразу же вернет () (что является версией void Rust).

Другими словами, это не «выполнение не выполняется в вызывающем потоке до первого ожидания», это «выполнение не запускается до тех пор, пока оно не будет явно порождено (например, с tokio::run

Ничего не происходит до тех пор, пока вызывающий объект не ожидает возвращаемого значения FooAsync, после чего тело FooAsync выполняется до тех пор, пока не будет выполнено ожидание или оно не вернется.

Ик. Это кажется неудачным. Часто я могу не дождаться чего-то (часто из-за отмены и композиции с задачами). Как разработчик, я все равно был бы признателен за ранние ошибки (что является одной из наиболее частых причин, по которой люди хотят, чтобы выполнение приближалось к ожиданию).

Это означает, что мы застряли либо с а) семантикой, подобной генератору C #, где код не запускается при вызове, либо б) семантикой, подобной корутинам Kotlin, где вызов функции также немедленно и неявно ожидает ее (с подобным замыканию async {. .} блоки, когда вам действительно нужно одновременное выполнение).

Учитывая это, я бы предпочел первое, чем второе. Только моя личная привилегия. Если подход kotlin кажется более естественным для вашего домена, сделайте это!

@CyrusNajmabadi Ick . Это кажется неудачным. Часто я могу не дождаться чего-то (часто из-за отмены и композиции с задачами). Как разработчик, я все равно был бы признателен за ранние ошибки (что является одной из наиболее частых причин, по которой люди хотят, чтобы выполнение приближалось к ожиданию).

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

В стиле Rust / Haskell / F # либо запускается Future (с правильной обработкой ошибок), либо он не запускается вообще. Затем вы замечаете, что он не работает, поэтому исследуете и исправляете его. Я считаю, что это приводит к более надежному коду.

@Pauan @rpjohnst Спасибо за объяснения. Мы тоже учли эти подходы. Но на практике это оказалось не так желательно.

В тех случаях, когда вы не хотели, чтобы он «на самом деле что-либо делал. Его нужно создавать вручную», мы сочли более понятным моделировать это как возвращение чего-то, что генерирует задачи по запросу. то есть что-то простое, как Func<Task> .

Я чувствую прямо противоположное. По моему опыту работы с JavaScript, очень часто забывают использовать ожидание.

C # действительно работает, чтобы гарантировать, что вы либо ждали, либо иным образом использовали задачу разумно.

но ошибки будут проглочены

Это противоположно тому, что я говорю. Я говорю, что хочу, чтобы код выполнялся с нетерпением, чтобы ошибки сразу же возникали, даже в том случае, если я никогда не дойду до выполнения кода в задаче. То же самое и с итераторами. Я бы предпочел знать, что я создавал его неправильно в тот момент, когда я вызываю функцию, по сравнению с потенциально гораздо дальше по строке, если / когда итератор транслируется.

Затем вы замечаете, что он не работает, поэтому исследуете и исправляете его.

В сценариях, о которых я говорю, «не работает» вполне разумно. В конце концов, мое приложение может в любой момент решить, что ему не нужно запускать задачу. Это не та ошибка, которую я описываю. Ошибка, которую я описываю, заключается в том, что я не прошел проверку, и я хочу узнать об этом как можно ближе к точке, где я логически создал работу, а не к точке, когда работа действительно должна быть запущена. Учитывая, что это модели для описания асинхронной обработки, часто бывает так, что они находятся далеко друг от друга. Поэтому ценно как можно раньше получать информацию о проблемах, которые возникают.

Как уже упоминалось, это тоже не гипотетически. То же самое происходит с потоками / итераторами. Люди часто их создают, но потом не осознают. Для людей было дополнительным бременем отслеживать эти вещи до их источника. Вот почему так много API (включая hte BCL) теперь должны выполнять разделение между синхронной / ранней работой и фактической отложенной / ленивой работой.

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

Я могу понять стремление к ранним ошибкам, но меня смущает: в какой ситуации вы когда-нибудь «не дойдете до появления Future »?

Future s работают в Rust следующим образом: вы составляете Future вместе различными способами (включая async / await, включая параллельные комбинаторы и т. одиночный слитный Future который содержит все вложенные Future s. А затем на верхнем уровне вашей программы ( main ) вы затем используете tokio::run (или аналогичный) для его создания.

Помимо этого единственного вызова tokio::run в main , вы обычно не будете порождать Future s вручную, вместо этого вы просто создаете их. И композиция, естественно, обрабатывает создание / обработку ошибок / отмену / и т. Д. правильно.

Я также хочу кое-что прояснить. Когда я говорю что-то вроде:

Но на практике это оказалось не так желательно.

Я говорю очень конкретно о вещах, связанных с нашим языком / платформой. Я могу только дать представление о решениях, которые имели смысл для C # /. Net / CoreFx и т. Д. Возможно, ваша ситуация иная, и то, что вы хотите оптимизировать, и типы подходов, которые вы должны использовать, полностью другое направление.

Я могу понять стремление к ранним ошибкам, но меня смущает: в какой ситуации вы когда-нибудь «не дойдете до создания будущего»?

Все время :)

Рассмотрим, как написан сам Roslyn (кодовая база компилятора C # / VB / IDE). Он сильно асинхронен и интерактивен . т. е. основной вариант использования должен использоваться совместно со многими клиентами, имеющими к нему доступ. Сервисы Cliest обычно взаимодействуют с пользователем с помощью множества функций, многие из которых решают, что им больше не нужно выполнять работу, которую они изначально считали важной, из-за того, что пользователь выполняет любое количество действий. Например, когда пользователь печатает, мы выполняем множество составлений задач и манипуляций, и мы можем в конечном итоге решить даже не приступить к их выполнению, потому что через несколько мс позже произошло другое событие.

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

Разве это не решается только отменой?

И композиция, естественно, обрабатывает создание / обработку ошибок / отмену / и т. Д. правильно.

Это просто звучит так, как будто у нас есть две очень разные модели для представления вещей. Ничего страшного :) Мои объяснения следует рассматривать в контексте выбранной нами модели. Они могут не иметь смысла для выбранной вами модели.

Это просто звучит так, как будто у нас есть две очень разные модели для представления вещей. Ничего страшного :) Мои объяснения следует рассматривать в контексте выбранной нами модели. Они могут не иметь смысла для выбранной вами модели.

Безусловно, я просто пытаюсь понять вашу точку зрения, а также объясняю нашу точку зрения. Спасибо, что нашли время объяснить вещи.

Разве это не решается только отменой?

Отмена - это ортогональное понятие асинхронности (для нас). Обычно они используются вместе. Но ни одно из них не требует другого.

У вас может быть система полностью без отмены, и это может быть просто случай, когда вы просто никогда не дойдете до запуска кода, который «ожидает» задач, которые вы составили. т.е. по логической причине ваш код может просто выглядеть следующим образом: «Мне не нужно ждать, я просто собираюсь сделать что-нибудь еще». Ничто в задачах (в нашем мире) не диктует и не требует, чтобы можно было ожидать выполнения этой задачи. В такой системе я хотел бы получить раннюю проверку.

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

Думаю, есть разумные аргументы в пользу обоих направлений. Но я считаю, что у синхронного подхода больше плюсов, чем минусов. Конечно, если синхронный подход буквально не подходит из-за того, как ваш реальный impl хочет работать, то это, похоже, отвечает на вопрос о том, что вам нужно делать: D

Другими словами, я не думаю, что ваш подход здесь плохой. И если у этой модели есть сильные преимущества, которые, по вашему мнению, подходят для Rust, тогда обязательно сделайте это :)

У вас может быть система полностью без отмены, и это может быть просто случай, когда вы просто никогда не дойдете до запуска кода, который «ожидает» задач, которые вы составили. т.е. по логической причине ваш код может просто выглядеть следующим образом: «Мне не нужно ждать, я просто собираюсь сделать что-нибудь еще».

Лично я считаю, что с этим лучше всего справиться с помощью обычной логики if/then/else :

async fn foo() {
    if some_condition {
        await!(bar());
    }
}

Но, как вы говорите, это совсем другая перспектива по сравнению с C #.

Лично я считаю, что с этим лучше всего справиться с помощью обычной логики if / then / else:

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

В наших доменах «ожидание» происходит тогда, когда человеку «нужно значение», что является другим определением / компонентом / и т. Д. из решения о том, "стоит ли мне начинать работу над ценностью?"

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

Anways, я откажусь от мнения о синхронизации / асинхронности. Ясно, что здесь задействованы очень разные модели. :)

Что касается приоритета, я дал некоторую информацию о том, как C # думает о вещах. Надеюсь, это поможет. Дайте мне знать, если вам нужна дополнительная информация.

@CyrusNajmabadi Да, ваши идеи были весьма полезны. Лично я согласен с вами, что await? foo - это правильный путь (хотя мне также нравится "явное предложение async ").

Кстати, если вам нужно одно из лучших экспертных мнений по всем тонкостям модели .net, касающимся моделирования работы async / sync, и всем плюсам и минусам этой системы, то @stephentoub . Он был бы примерно в 100 раз лучше меня в объяснении вещей, разъяснении плюсов и минусов и, вероятно, был бы в состоянии глубоко погрузиться в модели с обеих сторон. Он хорошо знаком с подходом .net здесь (включая сделанный и отклоненный выбор), а также с тем, как он должен был развиваться с самого начала. Он также болезненно осведомлен о затратах на производительность подходов, которые использует .net (что является одной из причин, по которой ValueTask сейчас существует), и я полагаю, что вы, ребята, думаете, в первую очередь, с вашим стремлением к нулевому / низкому уровню. -затратные абстракции.

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

Я бы все равно проголосовал за await? future даже если он выглядит немного незнакомым. Есть ли какие-то недостатки в их создании?

Вот еще один тщательный анализ плюсов и минусов холодного (F #) и горячего (C #, JS) асинхронного кода: http://tomasp.net/blog/async-csharp-differences.aspx

Теперь существует новый RFC для макросов postfix, который позволяет экспериментировать с postfix await без специального изменения синтаксиса: https://github.com/rust-lang/rfcs/pull/2442

await {} - мой любимый здесь, он напоминает unsafe {} плюс показывает приоритет.

let value = await { future }?;

@seunlanlege
да, это воспоминание, поэтому у людей есть ложное предположение, что они могут писать такой код

let value = await {
   let val1 = future1;
   future2(val1)
}

Но они не могут.

@Pzixel
если я вас правильно понимаю, вы предполагаете, что люди будут предполагать, что фьючерсы неявно ожидаются внутри блока await {} ? Я не согласен с этим. await {} будет ожидать только выражения, которое вычисляет блок.

let value = await {
    let future = create_future();
    future
};

И это должен быть образец, который не приветствуется.

упрощенный

let value = await { create_future() };

Вы предлагаете утверждение, в котором более одного выражения «не следует поощрять». Вы не видите в этом ничего плохого?

Выгодно ли делать шаблон await (кроме ref т. Д.)?
Что-то типа:

let await n = bar();

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

Согласно текущей странице https://doc.rust-lang.org/nightly/std/task/index.html , мод задачи состоит как из реэкспорта из libcore, так и из реэкспорта для liballoc, что делает результат немного ... неоптимально. Надеюсь, с этим как-нибудь справятся, прежде чем он стабилизируется.

Я взглянул на код. И у меня есть несколько предложений:

  • [x] Признак UnsafePoll и перечисление Poll имеют очень похожие имена, но не связаны между собой. Я предлагаю переименовать UnsafePoll , например, в UnsafeTask .
  • [x] В контейнере Futures код был разделен на разные подмодули. Теперь большая часть кода сгруппирована в task.rs что затрудняет навигацию. Предлагаю снова разделить его.
  • [x] TaskObj#from_poll_task() имеет странное имя. Я предлагаю вместо этого назвать его new()
  • [x] TaskObj#poll_task может быть просто poll() . Поле с именем poll может называться poll_fn что также предполагает, что это указатель на функцию.
  • Waker может использовать ту же стратегию, что и TaskObj и поместить vtable в стек. Просто идея, не знаю, хотим ли мы этого. Было бы это быстрее, потому что это немного менее косвенно?
  • [] dyn теперь стабильна в бета-версии. Код, вероятно, должен использовать dyn там, где он применяется

Я тоже могу предоставить пиар для этого материала. @cramertj @aturon не стесняйтесь обращаться ко мне через Discord, чтобы обсудить детали.

как насчет того, чтобы просто добавить метод await() для всех Future ?

    /// just like and_then method
    let x = f.and_then(....);
    let x = f.await();

    await f?     =>   f()?.await()
    await? f     =>   f().await()?

/// with chain invoke.
let x = first().await().second().await()?.third().await()?
let x = first().await()?.second().await()?.third().await()?
let x = first()?.await()?.second().await()?.third().await()?

@zengsai Проблема в том, что await не работает как обычный метод. Фактически, подумайте, что будет делать метод await , если он не находится в блоке / функции async . Методы не знают, в каком контексте они выполняются, поэтому это не может вызвать ошибку компиляции.

@xfix это вообще не так. Компилятор может делать все, что хочет, и в этом случае может обрабатывать вызов метода специально. Вызов стиля метода решает проблему предпочтений, но это неожиданно (await не работает таким образом на других языках) и, вероятно, было бы уродливым взломом в компиляторе.

@elszben То, что компилятор может делать все, что хочет, не означает, что он должен делать все, что хочет.

future.await() звучит как обычный вызов функции, хотя это не так. Если вы хотите пойти по этому пути, синтаксис future.await!() предложенный где-то выше, допускает ту же семантику и четко помечает макросом «Я знаю, что здесь происходит что-то странное».

Изменить: сообщение удалено

Я переместил этот пост в RFC фьючерсов. Ссылка на сайт

Кто-нибудь смотрел взаимодействие между async fn и #[must_use] ?

Если у вас есть async fn , его вызов напрямую не запускает кода и возвращает Future ; похоже, что все async fn должны иметь неотъемлемый #[must_use] на «внешнем» типе impl Future , поэтому вы не можете вызывать их, не выполняя что-то с Future .

Вдобавок к этому, если вы сами прикрепите #[must_use] к async fn , похоже, что это должно применяться к возврату внутренней функции. Итак, если вы напишете #[must_use] async fn foo() -> T { ... } , тогда вы не сможете написать await!(foo()) не сделав что-нибудь с результатом ожидания.

Кто-нибудь смотрел взаимодействие между async fn и # [must_use]?

Для других, заинтересованных в этом обсуждении, см. Https://github.com/rust-lang/rust/issues/51560.

Я думал о том, как реализованы асинхронные функции, и понял, что эти функции не поддерживают ни рекурсию, ни взаимную рекурсию.

Что касается синтаксиса ожидания, я лично отношусь к макросам после исправления, без неявного подхода ожидания, из-за его простой цепочки и того, что его также можно использовать как вызов метода

@ warlord500 вы полностью игнорируете весь опыт миллионов разработчиков, описанный выше. Вы не хотите связывать await .

@Pzixel, пожалуйста, не ветку или то, что хочу.
Я знаю, что некоторые участники могут не захотеть разрешать цепочки ожиданий, но некоторые из нас
разработчики, которые делают. Я не уверен, откуда вы вообще взяли, что я игнорировал
Мнения разработчиков, мой комментарий только уточнял мнение члена сообщества и причины, по которым я придерживаюсь этого мнения.

РЕДАКТИРОВАТЬ : если у вас есть разногласия, поделитесь, пожалуйста! Мне любопытно, почему вы говорите
мы не должны позволять связывать ожидания с помощью такого метода, как синтаксис?

@ warlord500, потому что команда MS делилась своим опытом с тысячами клиентов и миллионами разработчиков. Я сам это знаю, потому что я пишу код async / await на ежедневной основе, и вы никогда не захотите связывать их. Вот точная цитата, если хотите:

Мы много думали о приоритете с помощью await и опробовали множество форм, прежде чем установить ту форму, которую мы хотели. Одна из основных вещей, которые мы обнаружили, заключалась в том, что для нас и для клиентов (внутренних и внешних), которые хотели использовать эту функцию, люди редко действительно хотели «связать» что-либо помимо своего асинхронного вызова. Другими словами, люди, похоже, сильно тяготели к тому, чтобы «ожидание» было самой важной частью любого полного выражения, и, таким образом, оно было ближе к вершине. Примечание: под «полным выражением» я подразумеваю такие вещи, как выражение, которое вы получаете в верхней части выражения-выражения, или выражение справа от назначения верхнего уровня, или выражение, которое вы передаете как «аргумент» чему-либо.

Тенденция к желанию «продолжить» с «await» внутри выражения была редкой. Иногда мы видим такие вещи, как (await expr).M() , но они кажутся менее распространенными и менее желательными, чем количество людей, выполняющих await expr.M() .

Я сейчас очень запутался. Если я правильно вас понимаю, мы не должны поддерживать
await post-fix easy chain style, потому что он обычно не используется? вы видите ожидание как наиболее важную часть выражения.
В данном случае я лишь предполагаю, что правильно вас понимаю.
Если я ошибаюсь, не стесняйтесь поправлять меня.

Кроме того, вы можете разместить ссылку на то, где вы получили цитату,
Спасибо.

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

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

Кроме того, вы можете разместить ссылку на то, где вы получили цитату,
Спасибо.

Но ... но вы сказали, что прочитали всю ветку ... 😃

Но у меня нет проблем с тем, чтобы поделиться им: https://github.com/rust-lang/rust/issues/50547#issuecomment -388939886. Я предлагаю вам прочитать все сообщения Cyrus, это действительно опыт всей экосистемы C # /. Net, это бесценный опыт, который может повторно использовать Rust.

иногда ждать самой важной части выражения

Цитата ясно говорит об обратном 😄 И вы знаете, я и сам испытываю то же чувство, когда ежедневно пишу async / await.

У вас есть опыт работы с async / await? Не могли бы вы тогда поделиться, пожалуйста?

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

@Pzixel Я благодарен вам за то, что вы поделились информацией о своем опыте и опыте других, используя async / await , но, пожалуйста, относитесь с уважением к другим участникам. Вам не нужно критиковать уровень опыта других, чтобы ваши собственные технические моменты были услышаны.

Примечание модератора: @Pzixel Персональные атаки на членов сообщества не допускаются. Я отредактировал это из вашего комментария. Не делай это опять. Если у вас есть вопросы о нашей политике модерации, напишите нам по адресу [email protected].

@crabtw Я никого не критиковал. Приносим извинения за возможные неудобства.

Я спросил об опыте однажды, когда хотел понять, есть ли у человека реальная потребность в цепочке 'await'ов или это его экстраполяция сегодняшних функций. Я не хотел апеллировать к авторитету, это просто полезная информация, где я могу сказать: «нужно самому попробовать и самому осознать эту истину». Ничего обидного здесь нет.

Личные нападения на членов сообщества не допускаются. Я отредактировал это из вашего комментария.

Никаких личных выпадов. Как я вижу, вы прокомментировали мою ссылку о отрицательных голосах. Ну, это был просто мой ответ на мой пост против, ничего особенного. Поскольку он был удален, также разумно удалить эту ссылку (это может даже сбить с толку дальнейших читателей), так что спасибо за удаление этого.

Спасибо за ссылку. Я действительно хотел упомянуть, что вы не должны воспринимать то, что я говорю, как «евангелие» :) Rust и C # - это разные языки с разными сообществами, парадигмами и идиомами. Вы должны обязательно сделать лучший выбор для вашего языка. Я действительно надеюсь, что мои слова будут полезны и могут дать понимание. Но всегда будьте открыты для разных способов делать что-то.

Я надеюсь, что вы придумали что-нибудь потрясающее для Rust. Тогда мы можем увидеть , что вы сделали , и воруют любезно принять его для C # :)

Насколько я могу судить, связанный аргумент в основном говорит о приоритете await и, в частности, утверждает, что имеет смысл анализировать await x.y() как await (x.y()) а не (await x).y() потому что пользователь будет чаще хотеть и ожидать прежнюю интерпретацию (и интервал также предполагает эту интерпретацию). И я был бы склонен согласиться, хотя я также заметил, что синтаксис типа await!(x.y()) устраняет двусмысленность.

Однако я не думаю, что это дает какой-либо конкретный ответ относительно ценности цепочки вроде x.y().await!().z() .

Цитируемый комментарий интересен отчасти потому, что в Rust есть большая разница, которая была одним из важных факторов, задерживающих наше выяснение окончательного синтаксиса ожидания: в C # нет оператора ? , поэтому у них нет кода, который нужно написать (await expr)? . Они описывают (await expr).M() как действительно необычное, и я склонен думать, что это будет верно и в Rust, но единственное исключение из этого, с моей точки зрения, - ? , которое будет очень распространено. потому что многие фьючерсы будут оцениваться по результатам (например, все существующие прямо сейчас).

@withoutboats да, верно. Еще раз процитирую эту часть:

единственное исключение из этого, с моей точки зрения, это?

Если есть только исключение, то кажется разумным создать await? foo как ярлык для (await foo)? и получить лучшее из обоих миров.

Прямо сейчас, по крайней мере, предложенный синтаксис await!() позволит однозначно использовать ? . Мы можем позаботиться о более коротком синтаксисе для комбинации await и ? если и когда мы решим изменить базовый синтаксис для await . (И в зависимости от того, на что мы его заменим , у нас может вообще не быть проблемы.)

@joshtriplett эти дополнительные фигурные скобки устраняют двусмысленность, но они действительно очень тяжелые. Например, поиск по моему текущему проекту:

Matching lines: 139 Matching files: 10 Total files searched: 77

У меня 139 ожиданий в 2743 sloc. Возможно, это не имеет большого значения, но я думаю, что мы должны рассматривать альтернативу без опоры как более чистую и лучшую. Как уже было сказано, ? - единственное исключение, поэтому мы могли бы легко использовать await foo без фигурных скобок и ввести специальный синтаксис только для этого особого случая. Это не имеет большого значения, но может сэкономить некоторые скобки для проекта LISP.

Я создал сообщение в блоге о том, почему я считаю, что асинхронные функции должны использовать подход внешнего возвращаемого типа для своей подписи. Наслаждайся чтением!

https://github.com/MajorBreakfast/rust-blog/blob/master/posts/2018-06-19-outer-return-type-approach.md

Я не следил за всеми обсуждениями, поэтому не стесняйтесь указать мне, где это уже обсуждалось бы, если бы я это пропустил.

Есть еще одна проблема, связанная с подходом к внутреннему возвращаемому типу: как будет выглядеть синтаксис для Stream s, когда он будет указан? Я бы подумал, что async fn foo() -> impl Stream<Item = T> будет хорошо выглядеть и соответствовать async fn foo() -> impl Future<Output = T> , но не будет работать с подходом внутреннего возвращаемого типа. И я не думаю, что мы захотим вводить ключевое слово async_stream .

@Ekleog Stream нужно будет использовать другое ключевое слово. Он не может использовать async потому что impl Trait работает наоборот. Он может только гарантировать, что определенные черты реализованы, но сами черты должны быть уже реализованы в базовом конкретном типе.

Однако подход внешнего возвращаемого типа пригодится, если мы однажды захотим добавить функции асинхронного генератора:

async_gen fn foo() -> impl AsyncGenerator<Yield = i32, Return = ()> { yield 1; ... }

Stream можно реализовать для всех асинхронных генераторов с помощью Return = () . Это делает возможным:

async_gen fn foo() -> impl Stream<Item = i32> { yield 1;  ... }

Примечание. Генераторы уже работают по ночам, но они не используют этот синтаксис. В отличие от фьючерсов 0.3 Stream в настоящее время также не поддерживают закрепление.

Изменить: этот код ранее использовал Generator . Я пропустил разницу между Stream и Generator . Потоки асинхронные. Это означает, что они могут, но не обязаны давать значение. Они могут ответить Poll::Ready или Poll::Pending . С другой стороны, Generator всегда должно уступать или завершаться синхронно. Теперь я изменил его на AsyncGenerator чтобы отразить это.

Edit2: @Ekleog Текущая реализация генераторов использует синтаксис без маркера и, кажется, обнаруживает, что это должен быть генератор, ища yield внутри тела. Это означает, что вы были бы правы, если бы сказали, что async можно использовать повторно. Другой вопрос, разумен ли такой подход. Но я думаю, это уже для другой темы ^^ '

В самом деле, я думал, что async можно использовать повторно, только потому, что async , согласно этому RFC, будет разрешено только с Future s, и, таким образом, может обнаруживать он генерирует Stream , глядя на возвращаемый тип (который должен быть либо Future либо Stream ).

Причина, по которой я поднимаю этот вопрос сейчас, заключается в том, что если мы хотим иметь одно и то же ключевое слово async для генерации как Future s, так и Stream s, тогда я думаю, что внешний возврат Типовой подход был бы намного чище, потому что он был бы явным, и я не думаю, что кто-то ожидал бы, что async fn foo() -> i32 приведет к потоку i32 (что было бы возможно, если бы тело содержало yield и был выбран подход внутреннего возвращаемого типа).

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

@rpjohnst Я поднял этот вопрос, потому что подход внешнего возвращаемого типа позволяет легко установить два связанных типа.

Мы не хотим устанавливать два связанных типа. Stream по-прежнему представляет собой единственный тип, а не impl Iterator<Item=impl Future>> или что-то в этом роде.

@rpjohnst Я имел в виду связанные типы Yield и Return (асинхронных) генераторов

gen fn foo() -> impl Generator<Yield = i32, Return = ()> { ... }

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

// generator
fn foo() -> T yields Y

// generator that implements Iterator
fn foo() yields Y

// async generator
async fn foo() -> T yields Y

// async generator that implements Stream
async fn foo() yields Y

В более общем плане, я думаю, у нас должно быть больше опыта в реализации, прежде чем мы пересмотрим какие-либо решения, принятые в RFC. Мы обходим те же аргументы, которые мы уже сделали, нам нужен опыт работы с функцией, предложенной RFC, чтобы увидеть, требуется ли повторное взвешивание.

Я хотел бы полностью согласиться с вами, но просто задаюсь вопросом: если я правильно прочитал ваш комментарий, стабилизация синтаксиса async / await будет ждать достойного синтаксиса и реализации для асинхронных потоков и получения опыта с ними двумя? (поскольку после его стабилизации невозможно будет переключаться между внешними типами возврата и внутренними типами возврата)

Я думал, что async / await ожидается для Rust 2018, и не надеялся, что к тому времени будут готовы асинхронные генераторы, но…?

(Кроме того, мой комментарий был задуман только как дополнительный аргумент к сообщению в блоге @MajorBreakfast , но, похоже, он полностью стер обсуждение этой темы ... это вовсе не было моей целью, и я полагаю, что дебаты должны снова сосредоточиться на это сообщение в блоге?)

Узкий вариант использования ключевого слова await все еще меня смущает. (Например, будущее против потока против генератора)

Разве ключевого слова yield не достаточно для всех случаев использования? Как в

{ let a = yield future; println(a) } -> Future

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

(Кстати, мы сделали это на глиняном языке)

@aep await не дает будущего от генератора - он приостанавливает выполнение Future и возвращает управление вызывающей стороне.

@cramertj хорошо, что он мог бы сделать именно это (вернуть будущее, которое содержит продолжение после ключевого слова yield), что является гораздо более широким вариантом использования.
но я думаю, я немного опоздал на вечеринку для этого обсуждения? :)

@aep Причина для ключевого слова, await заключается в возможности комбинирования с будущим специфичным для генератора ключевым словом yield . Мы хотим поддерживать асинхронные генераторы, а это означает две независимые «области» продолжения.

Кроме того, он не может вернуть будущее, которое содержит продолжение, потому что фьючерсы Rust основаны на опросах, а не на обратных вызовах, по крайней мере, частично из соображений управления памятью. Гораздо проще poll изменять отдельный объект, чем yield перебрасывать ссылки на него.

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

@sackery Это часть внутренней части языка и не может быть реализована только как библиотека.

так что просто сделайте это ключевым словом, как nim, в C #!

Вопрос: какой должна быть сигнатура закрытий async без перемещения, которые захватывают значения по изменяемой ссылке? В настоящее время они просто запрещены. Похоже, нам нужен какой-то подход GAT, который позволил бы заимствовать закрытие до тех пор, пока не умрет будущее, например:

trait AsyncFnMut {
    type Output<'a>: Future;
    fn call(&'a mut self, args: ...) -> Self::Output<'a>;
}

@cramertj здесь общая проблема с возвратом изменяемых ссылок на захваченное окружение замыкания. Возможно решение не нужно привязать к async fn?

@withoutboats правильно, это будет гораздо более распространено в ситуациях async чем в других местах.

Как насчет fn async вместо async fn ?
Мне нравится let mut больше, чем mut let .

fn foo1() {
}
fn async foo2() {
}
pub fn foo3() {
}
pub fn async foo4() {
}

После того, как вы выполните поиск в pub fn , вы все равно сможете найти все общедоступные функции в исходном коде.но в настоящее время синтаксиса нет.

fn foo1() {
}
async fn foo2() {
}
pub fn foo3() {
}
pub async fn foo4() {
}

Это предложение не очень важно, это дело личного вкуса.
Так что я уважаю мнение всех вас :)

Я считаю, что все модификаторы должны стоять перед fn. Понятно и так, как это делается на других языках. Это просто здравый смысл.

@Pzixel Я знаю, что модификаторы доступа должны fn потому что это важно.
но я думаю, что async , вероятно, нет.

@xmeta Я не видел, чтобы эта идея предлагалась раньше. Вероятно, мы хотим поставить async перед fn чтобы быть последовательными, но я думаю, что важно рассмотреть все варианты. Спасибо за публикацию!

// Status quo:
pub unsafe async fn foo() {} // #![feature(async_await, futures_api)]
pub const unsafe fn foo2() {} // #![feature(const_fn)]

@MajorBreakfast Спасибо за ответ, я так думал.

{ Public, Private } ⊇ Function  → put `pub` in front of `fn`
{ Public, Private } ⊇ Struct    → put `pub` in front of `struct`
{ Public, Private } ⊇ Trait     → put `pub` in front of `trait`
{ Public, Private } ⊇ Enum      → put `pub` in front of `enum`
Function ⊇ {Async, Sync}        → put `async` in back of `fn`
Variable ⊇ {Mutable, Imutable}  → put `mut` in back of `let`

@xmeta @MajorBreakfast

async fn неделим, он представляет собой асинхронную функцию。

async fn - это целое.

Вы ищете pub fn что означает, что вы ищете общедоступную функцию синхронизации.
Таким же образом вы ищете pub async fn что означает, что вы ищете общедоступную асинхронную функцию.

@ZhangHanDong

  • async fn определяет обычную функцию, возвращающую будущее. Все функции, возвращающие будущее, считаются «асинхронными». Указатели на функции async fn s и других функций, возвращающих будущее, идентичны °. Вот пример игровой площадки . Поиск по запросу «async fn» может найти только функции, использующие нотацию, он не найдет все асинхронные функции.
  • Поиск по запросу pub fn не найдет функций unsafe или const .

° Конкретный тип, возвращаемый async fn конечно, анонимен. Я имею в виду, что они оба возвращают тип, реализующий Future

@xmeta обратите внимание, что mut не "идет после let", или, скорее, что mut не изменяет let . let принимает шаблон, то есть

let PATTERN = EXPRESSION;

mut является частью PATTERN , а не самого let . Например:

// one is mutable one is not
let (mut a, b) = (1, 2);

@steveklabnik Я понимаю. Я просто хотел показать связь между иерархической структурой и порядком слов. Спасибо

Что люди думают о желаемом поведении return и break внутри блоков async ? В настоящее время return возвращается из асинхронного блока - если мы вообще разрешим return , это действительно единственный возможный вариант. Мы могли бы полностью заблокировать return и использовать что-то вроде 'label: async { .... break 'label x; } для возврата из асинхронного блока. Это также связано с дискуссией о том, следует ли использовать ключевое слово break или return для функции разбиения на блоки (https://github.com/rust-lang/rust/issues/ 48594).

Я за то, чтобы разрешить return . Основная проблема для запрета заключается в том, что это может сбить с толку, потому что оно возвращается не из текущей функции, а из блока async. Я, однако, сомневаюсь, что это будет сбивать с толку. Замыкания уже позволяют return и я никогда не находил это запутанным. Узнать, что return применяется к асинхронным блокам, ИМО просто, и разрешение этого ИМО весьма ценно.

@cramertj return всегда должен выходить из содержащей функцию, а не из внутреннего блока; если это не имеет смысла, что, похоже, не работает, тогда return не должно работать вообще.

Использование break для этого кажется неудачным, но, учитывая, что у нас, к сожалению, есть значение label-break-value, это, по крайней мере, согласуется с этим.

Планируются ли еще асинхронные перемещения и закрытия? Следующее взято из RFC:

// closure which is evaluated immediately
async move {
     // asynchronous portion of the function
}

и далее вниз по странице

async { /* body */ }

// is equivalent to

(async || { /* body */ })()

что делает return выровненным с замыканиями, и кажется довольно простым для понимания и объяснения.

Планируется ли RFC break-to-block разрешить выпрыгивать из внутреннего затвора с меткой? Если нет (и я не предлагаю, чтобы это было разрешено), было бы очень неудачно запретить последовательное поведение returns , а затем использовать альтернативу, которая также несовместима с rfc break-to-blocks.

@memoryruins async || { ... return x; ... } должен работать абсолютно. Я говорю, что async { ... return x; ... } не должен, именно потому, что async не является закрытием. return имеет очень конкретное значение: «возврат из содержащей функции». Замыкания - это функция. асинхронных блоков нет.

@memoryruins Оба они уже реализованы.

@joshtriplett

асинхронных блоков нет.

Думаю, я все еще думаю о них как о функциях в том смысле, что они являются телом с отдельно определенным контекстом выполнения из блока, который их содержит, поэтому для меня имеет смысл, что return является внутренним по отношению к async блок. Путаница здесь кажется в основном синтаксической, поскольку блоки обычно являются просто оболочками для выражения, а не вещами, которые переносят код в новый контекст выполнения, например || и async do.

@cramertj Тем не менее, синтаксис важен.

Подумайте об этом таким образом. Если у вас есть что - то , что не выглядит как функция (или , как закрытие, и вы использовали для распознавания замыкания как функция), и вы видите return , где твой ментальный синтаксический анализатор думает , что это идет?

Все, что захватывает return затрудняет чтение чужого кода. Люди по крайней мере привыкли к мысли, что break возвращается в какой-то родительский блок, и им нужно будет прочитать контекст, чтобы узнать, какой блок. return всегда был большим молотком, возвращаемым всей функцией.

Если бы они не обрабатывались так же, как немедленно оцениваемые замыкания, я согласен, что return будет непоследовательным, особенно синтаксически. Если ? в асинхронных блоках уже было решено (RFC по-прежнему говорит, что это еще не определено), то я предполагаю, что это будет согласовано с этим.

@joshtriplett мне кажется произвольным говорить, что вы можете распознавать функции и замыкания (которые синтаксически сильно различаются) как «возвращаемые области», но асинхронные блоки не могут быть распознаны по одним и тем же линиям. Почему допустимы две различные синтаксические формы, а не три?

Ранее эта тема обсуждалась в RFC . Как я уже сказал, я выступаю за асинхронные блоки с использованием break _без необходимости указывать метку (нет возможности выйти из асинхронного блока во внешний цикл, чтобы не потерять выразительность).

@withoutboats Закрытие - это просто еще один вид функции; как только вы узнаете, что «замыкание - это функция», вы можете применить к замыканиям все, что вы знаете о функциях, включая « return всегда возвращается из содержащей функцию».

@ Nemo157 Даже если вы не пометили break target для блока async , вам нужно будет предоставить механизм (например, 'label: async ) для раннего возврата из цикла внутри асинхронного блока. .

@joshtriplett

Замыкание - это просто еще один вид функции; как только вы узнаете, что «замыкание - это функция», вы можете применить к замыканиям все, что вы знаете о функциях, включая «возврат всегда возвращается из содержащей функцию».

Я думаю, что async блоки - это тоже своего рода «функция» - функция без аргументов, которую можно запустить до завершения асинхронно. Это частный случай закрытий async , которые не имеют аргументов и были применены заранее.

@cramertj да, я предполагал, что любая неявная точка

Все, что затрудняет отслеживание потока управления и, в частности, переопределяет то, что означает return , создает большую нагрузку на возможность плавного чтения кода.

В том же духе стандартное руководство в C - «не пишите макросы, которые возвращаются из середины макроса». Или, как менее распространенный, но все же проблемный случай: если вы пишете макрос, который выглядит как цикл, break и continue должны работать внутри него. Я видел, как люди пишут макросы типа цикла, которые фактически включают два цикла, поэтому break работает не так, как ожидалось, и это очень сбивает с толку.

Я думаю, что асинхронные блоки - это тоже своего рода «функция»

Я думаю, что это перспектива, основанная на знании внутреннего устройства реализации.

Я вообще не рассматриваю их как функции.

Я вообще не рассматриваю их как функции.

@joshtriplett

Я подозреваю, что вы бы применили тот же аргумент, впервые придя к языку с замыканиями - что return не должно работать внутри замыкания, а внутри определяющей функции. И действительно, есть языки, принимающие такую ​​интерпретацию, например Scala.

@cramertj Я бы не стал, нет; для лямбда-выражений и / или функций, определенных в функции, кажется совершенно естественным, что они являются функцией. (Мое первое знакомство с ними было в Python, FWIW, где лямбды не могут использовать return а во вложенных функциях return возвращается из функции, содержащей return .)

Я думаю, что если знать, что делает асинхронный блок, интуитивно понятно, как должен себя вести return . Как только вы узнаете, что он представляет собой отложенное выполнение, становится ясно, что return не может применяться к функции. Понятно, что функция вернется уже к моменту запуска блока. ИМО, узнав, что это не должно быть большой проблемой. Мы должны хотя бы попробовать и посмотреть.

Этот RFC не предлагает, как ? -оператор и конструкции потока управления, такие как return , break и continue должны работать внутри асинхронных блоков.

Было бы лучше запретить использование любых операторов потока управления или отложить блоки до тех пор, пока не будет написан специальный RFC? Были упомянуты и другие желаемые особенности, которые будут обсуждаться позже. А пока у нас будут асинхронные функции, замыкания и await! :)

Я согласен с @memoryruins здесь, я думаю, что стоит создать еще один RFC, чтобы обсудить эти особенности более подробно.

Что вы думаете о функции, которая позволяет нам получить доступ к контексту из асинхронной fn, возможно, называемой core::task::context() ? При вызове из-за пределов асинхронной функции fn произошла бы просто паника. Я думаю, это было бы очень удобно, например, получить доступ к исполнителю для создания чего-либо.

@MajorBreakfast, эта функция называется lazy

async fn foo() -> i32 {
    await!(lazy(|ctx| {
        // do something with ctx
        42
    }))
}

Для чего-то более конкретного, например, нереста, вероятно, будут вспомогательные функции, которые сделают его более эргономичным.

async fn foo() -> i32 {
    let some_task = lazy(|_| 5);
    let spawned_task = await!(spawn_with_handle(some_task));
    await!(spawned_task)
}

@ Nemo157 На самом деле spawn_with_handle - вот где я бы хотел это использовать. При преобразовании кода в 0.3 я заметил, что spawn_with_handle на самом деле только будущее, потому что ему нужен доступ к контексту ( см. Код ). Я бы хотел добавить метод spawn_with_handle к ContextExt и сделать spawn_with_handle бесплатной функцией, которая работает только внутри асинхронных функций:

fn poll(self: PinMut<Self>, cx: &mut Context) -> Poll<Self::Output> {
     let join_handle = ctx.spawn_with_handle(future);
     ...
}
async fn foo() {
   let join_handle = spawn_with_handle(future); // This would use this function internally
   await!(join_handle);
}

Это устранило бы всю бессмыслицу с двойным ожиданием, которая у нас есть в настоящее время.

Если подумать, этот метод должен называться core::task::with_current_context() и работать немного по-другому, потому что, должно быть, невозможно сохранить ссылку.

Изменить: эта функция уже существует под именем get_task_cx . В настоящее время он находится в libstd по техническим причинам. Я предлагаю сделать его общедоступным API, как только его можно будет поместить в libcore.

Я сомневаюсь, что можно будет иметь функцию, которая может быть вызвана из функции, отличной от async которая может предоставить вам контекст из какой-либо родительской функции async после того, как она будет перемещена из TLS. В этот момент контекст, скорее всего, будет рассматриваться как скрытая локальная переменная внутри функции async , поэтому у вас может быть макрос, который обращается к контексту непосредственно в этой функции, но не будет возможности иметь spawn_with_handle волшебным образом вытягивает контекст из вызывающей стороны.

Итак, потенциально что-то вроде

fn spawn_with_handle(executor: &mut Executor, future: impl Future) { ... }

async fn foo() {
    let join_handle = spawn_with_handle(async_context!().executor(), future);
    await!(join_handle);
}

@ Nemo157 Я думаю, что вы правы: такая функция, как я предлагаю, скорее всего, не могла бы работать, если бы она не вызывалась напрямую из async fn. Возможно, лучший способ - сделать макрос spawn_with_handle который использует await внутри (например, select! и join! ):

async fn foo() {
    let join_handle = spawn_with_handle!(future);
    await!(join_handle);
}

Это выглядит красиво и может быть легко реализовано с помощью await!(lazy(|ctx| { ... })) внутри макроса.

async_context!() проблематичен, потому что он не может помешать мне сохранить ссылку на контекст в точках ожидания.

async_context!() является проблематичным, потому что не может помешать мне сохранить ссылку на контекст в точках ожидания.

В зависимости от реализации может. Если будут воскрешены полные аргументы генератора, их потребуется ограничить таким образом, чтобы вы в любом случае не могли сохранить ссылку через точку доходности, значение, стоящее за аргументом, будет иметь время жизни, которое выполняется только до точки доходности. Async / await просто унаследует это ограничение.

@ Nemo157 Вы имеете в виду что-то вроде этого?

let my_arg = yield; // my_arg lives until next yield

@Pzixel Извините, что разбудил _ возможно_ старую дискуссию, но я хотел бы добавить свои мысли.

Да, мне нравится, что синтаксис await!() устраняет двусмысленность при объединении его с такими вещами, как ? , но я также согласен с тем, что этот синтаксис раздражает тысячи раз печатать в одном проекте. Я также считаю, что это шумно, и чистый код важен.

Вот почему мне интересно, каков настоящий аргумент против суффиксного символа (который уже упоминался несколько раз раньше), например something_async()@ сравнить с чем-то с await , может быть потому, что await - ключевое слово, известное из других языков? @ может быть забавным, поскольку он напоминает a из await, но это может быть любой символ, который подошел бы идеально.

Я бы сказал, что такой выбор синтаксиса был бы логичным, поскольку нечто подобное произошло с try!() которое в основном превратилось в суффикс ? (я знаю, что это не совсем то же самое). Он краткий, легко запоминается и легко набирается.

Еще одна замечательная вещь в таком синтаксисе заключается в том, что поведение сразу становится понятным при сочетании с символом ? (по крайней мере, я так думаю). Взгляните на следующее:

// Await, then unwrap a Result from the future
awaiting_a_result()@?;

// Unwrap a future from a result, then await
result_with_future()?@;

// The real crazy can make it as funky as they want
magic()?@@?@??@; 
// - I'm joking, of course

В этом нет проблемы, как в случае с await future? , когда с первого взгляда не ясно, что произойдет, если вы не знаете о такой ситуации. И все же его реализация согласуется с ? .

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

  • возможно, он _ слишком краткий и менее заметный / подробный, в отличие от чего-то с await , что делает _ трудным_ обнаружение точек приостановки в функции.
  • возможно, он асимметричен с ключевым словом async , где одно - ключевое слово, а другое - символ. Хотя, await!() страдает той же проблемой, которая заключается в использовании ключа по сравнению с макросом.
  • выбор символа добавляет еще один синтаксический элемент, которому нужно научиться. Но, если предположить, что это может стать чем-то широко используемым, я не думаю, что это проблема.

@phaux также упомянул об использовании символа ~ . Однако я считаю, что набирать этого персонажа на многих раскладках клавиатуры довольно забавно, поэтому я бы рекомендовал отказаться от этой идеи.

Что вы думаете, ребята? Согласны ли вы, что это как-то похоже на то, как try!() _became_ ? ? Вы предпочитаете await или символ и почему? Я с ума сошел из-за того, что это обсуждает, или, может быть, я что-то упускаю?

Извините за неправильную терминологию, которую я, возможно, использовал.

Больше всего меня беспокоит синтаксис на основе сигил, что он может легко превратиться в суп из глифов, как вы любезно продемонстрировали. Любой, кто знаком с Perl (до 6), поймет, к чему я клоню. Избегать линейного шума - лучший способ двигаться вперед.

Тем не менее, может быть, лучший способ на самом деле точно такой try! ? То есть начните с явного макроса async!(foo) и, если возникнет необходимость, добавьте какую-нибудь сигилу, которая была бы сахаром для этого. Конечно, это откладывает проблему на потом, но async!(foo) вполне достаточно для первой итерации async / await, с тем преимуществом, что он относительно бесспорен. (и наличие прецедента try! / ? должно привести к появлению сигилы)

@withoutboats Я не читал всю эту

И, что касается оставшихся нерешенных вопросов, обращался ли кто-нибудь за помощью к экспертам за пределами сообщества Rust? Джо Даффи знает и очень заботится о параллелизме и хорошо разбирается в сложных деталях , и он выступил с основным докладом на RustConf , поэтому я подозреваю, что он, возможно,

@BatmanAoD первоначальная реализация была размещена в https://github.com/rust-lang/rust/pull/51580

В исходной ветке RFC были комментарии от ряда экспертов в области PLT, даже за пределами Rust :)

Я хотел бы предложить символ «$» для ожидания Futures, потому что время - деньги, и я хочу напомнить об этом компилятору.

Просто шутка. Не думаю, что наличие символа ожидания - хорошая идея. Суть Rust заключается в том, чтобы быть откровенным и дать людям возможность писать низкоуровневый код на мощном языке, который не позволит вам выстрелить себе в ногу. Символ намного более расплывчатый, чем макрос await! , и позволяет людям выстрелить себе в ногу другим способом, написав трудный для чтения код. Я бы уже сказал, что ? - это слишком далеко.

Я также не согласен с тем, чтобы ключевое слово async использовалось в форме async fn . Это подразумевает некую «предвзятость» в сторону асинхронности. Почему async заслуживает ключевого слова? Асинхронный код - это еще одна абстракция для нас, которая не всегда необходима. Я думаю, что атрибут async действует больше как «расширение» базового диалекта Rust, которое позволяет нам писать более мощный код.

Я не являюсь языковым архитектором, но у меня есть некоторый опыт написания асинхронного кода с использованием обещаний в JavaScript, и я думаю, что то, как это делается там, делает написание асинхронного кода приятным.

@steveklabnik Ах, хорошо, спасибо. Можем ли мы (/ должен ли я) обновить описание проблемы? Возможно, подпункт «начальная реализация» следует разделить на «реализацию без поддержки move » и «полную реализацию»?

Прорабатывается ли следующая итерация реализации в какой-нибудь публичной вилке / ветке? Или это не может продолжаться до тех пор, пока не будет принят RFC 2418?

Почему проблема синтаксиса async / await обсуждается здесь, а не в RFC?

@ c-edw Я думаю, что на вопрос о ключевом слове async отвечает " Какой цвет является вашей функцией?"

@parasyte Мне было предложено, что этот пост на самом деле является аргументом против всей идеи асинхронных функций без автоматически управляемого параллелизма в стиле зеленого потока.

Я не согласен с этой позицией, потому что зеленые потоки не могут быть реализованы прозрачно без (управляемой) среды выполнения, и у Rust есть веская причина поддерживать асинхронный код, не требуя этого.

Но похоже, что вы читаете пост, что семантика async / await в порядке, но можно ли сделать вывод о ключевом слове? Не могли бы вы подробнее остановиться на этом?

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

@parasyte А, ладно. Рад, что спросил - из-за неприятия автора дихотомии красный / синий я подумал, что вы говорите обратное!

Я хотел бы внести дополнительные пояснения, так как чувствую, что не совсем правильно это сделал.

Дихотомия неизбежна . Некоторые проекты пытались стереть его, сделав каждый вызов функции асинхронным, при этом вызовы функции синхронизации не существуют. Мидори - очевидный пример. А другие проекты пытались стереть эту дихотомию, скрывая асинхронные функции за фасадом функций синхронизации. gevent является примером такого рода.

У обоих одна и та же проблема; им по-прежнему нужна дихотомия, чтобы различать ожидание завершения асинхронной задачи и ее запуск в асинхронном режиме !

  • Midori представила не только ключевое слово await , но и ключевое слово async на сайте вызова функций .
  • gevent предоставляет gevent.spawn в дополнение к неявному ожиданию обычных вызовов функций.

Это была единственная причина, по которой я поднял статью о цвете функции, поскольку она отвечает на вопрос: «Почему async заслуживает ключевого слова?»

Что ж, даже синхронный код на основе потоков может различать «ожидание завершения задачи» (присоединение) и «запуск задачи» (порождение). Вы можете представить себе язык, где все асинхронно (с точки зрения реализации), но нет аннотации к await (потому что это поведение по умолчанию), а async Midori вместо этого является закрытием, переданным spawn API. Это ставит all-async на ту же синтаксическую / функциональную основу, что и all-sync.

Итак, хотя я согласен, что async заслуживает ключевого слова, мне кажется, что это больше, потому что Rust заботится о механизме реализации на этом уровне и по этой причине должен предоставлять оба цвета.

@rpjohnst Да, я читал ваши предложения. Концептуально это то же самое, что скрыть цвета a la gevent. Который я раскритиковал на форуме ржавчины в той же ветке; каждый вызов функции выглядит синхронным, что представляет особую опасность, когда функция одновременно синхронна и блокируется в асинхронном конвейере. Этот вид ошибки непредсказуем и представляет собой настоящую катастрофу для устранения неполадок.

Я не говорю конкретно о своем предложении, я говорю о языке, где все асинхронно. Таким образом вы

IIUC именно это и попыталась сделать Мидори. На этом этапе ключевые слова против замыканий - это всего лишь аргумент в пользу семантики.

12 июля 2018 г., 15:01, Рассел Джонстон [email protected]
написал:

Вы использовали наличие ключевых слов в качестве аргумента в пользу дихотомии
все еще существует в Мидори. Если вы удалите их, где будет дихотомия? В
синтаксис идентичен коду all-sync, но с возможностями async
код.

Потому что, когда вы вызываете асинхронную функцию, не дожидаясь ее результата, она
синхронно возвращает обещание. Чего можно ждать позже. 😐

_Wow, кто-нибудь знает что-нибудь о Мидори? Я всегда думал, что это закрытый проект, над которым практически не работают живые существа. Было бы интересно, если бы кто-нибудь из вас, ребята, написал об этом поподробнее ._

/не по теме

@Pzixel Ни одно живое существо еще не работает над этим, потому что проект был закрыт. Но в блоге Джо Даффи есть много интересных подробностей. Смотрите мои ссылки выше.

Здесь мы сошли с рельсов, и я чувствую, что повторяюсь, но это часть «наличия ключевых слов» - ключевое слово await . Если вы замените ключевые слова на API, такие как spawn и join , вы можете быть полностью асинхронными (как Midori), но без какой-либо дихотомии (в отличие от Midori).

Или, другими словами, как я сказал ранее, это не принципиально - у нас это есть только потому, что мы хотим выбора.

@CyrusNajmabadi, извините, что снова вот дополнительная информация о принятии решения.

Если ты не хочешь, чтобы тебя снова упомянули, просто скажи мне, пожалуйста. Я просто подумал, что тебе это может быть интересно.

Из канала Discord # wg-net :

@cramertj
Пища для размышлений: я часто пишу Ok::<(), MyErrorType>(()) в конце async { ... } блоков. возможно, мы можем что-то придумать, чтобы упростить ограничение типа ошибки?

@withoutboats
[...] возможно, мы хотим, чтобы это соответствовало [ try ]?

(Я помню относительно недавнее обсуждение того, как блоки try могут объявлять свой возвращаемый тип, но сейчас я не могу его найти ...)

упомянутые оттенки:

async -> io::Result<()> {
    ...
}

async: io::Result<()> {
    ...
}

async as io::Result<()> {
    ...
}

Одна вещь, которую try может сделать, что менее эргономично с async - это использовать привязку переменной или присвоение типа, например

let _: io::Result<()> = try { ... };
let _: impl Future<Output = io::Result<()>> = async { ... };

Ранее я высказывал идею о том, чтобы разрешить синтаксис, подобный fn, для признака Future , например Future -> io::Result<()> . Это сделало бы вариант с ручным вводом немного лучше, хотя в нем по-прежнему много символов:

let _: impl Future -> io::Result<()> = async {
}
async -> impl Future<Output = io::Result<()>> {
    ...
}

был бы моим выбором.

Он похож на существующий синтаксис закрытия :

|x: i32| -> i32 { x + 1 };

Изменить: И в конце концов, когда TryFuture станет возможным реализовать Future :

async -> impl TryFuture<Ok = i32, Error = ()> {
    ...
}

Edit2: Чтобы быть точным, приведенное выше будет работать с сегодняшними определениями черт. Это просто , что TryFuture Тип не столь полезным сегодня , потому что в настоящее время не реализует Future

@MajorBreakfast Почему -> impl Future<Output = io::Result<()>> а не -> io::Result<()> ? Мы уже выполняем обессахаривание возвращаемого типа для async fn foo() -> io::Result<()> , поэтому ИМО, если мы будем использовать синтаксис на основе -> кажется очевидным, что нам здесь нужен такой же сахар.

@cramertj Да, он должен быть последовательным. В моем сообщении выше вроде бы предполагается, что я могу убедить вас всех в превосходстве подхода с внешним возвращаемым типом 😁

В случае, если мы идем с async -> R { .. } то, по-видимому, мы должны также использовать try -> R { .. } а также использовать expr -> TheType в целом для описания типа. Другими словами, используемый нами синтаксис приписывания типов должен применяться повсюду единообразно.

@Centril Я согласен. Его можно использовать везде. Я просто больше не уверен, действительно ли -> - правильный выбор. У меня -> ассоциируется с возможностью вызова. А асинхронные блоки - нет.

@MajorBreakfast Я в целом согласен; Я думаю, мы должны использовать : для описания типа, поэтому async : Type { .. } , try : Type { .. } и expr : Type . Мы обсудили потенциальную двусмысленность в Discord, и я думаю, что мы нашли выход с : который имеет смысл ...

Другой вопрос по поводу Either enum. У нас уже есть Either в ящике futures . Это также сбивает с толку, потому что выглядит так же, как Either из ящика either когда это не так.

Поскольку Futures кажется объединенным в std (по крайней мере, самые основные его части), можем ли мы также включить туда Either ? Очень важно иметь их, чтобы иметь возможность возвращать impl Future из функции.

Например, я часто пишу такой код:

fn handler() -> impl Future<Item = (), Error = Bar> + Send {
    someFuture()
        .and_then(|x| {
            if condition(&x) {
                Either::A(anotherFuture(x))
            } else {
                Either::B(future::ok(()))
            }
        })
}

Я бы хотел написать это так:

async fn handler() -> Result<(), Bar> {
    let x = await someFuture();
    if condition(&x) {
        await anotherFuture(x);
    }
}

Но как я понимаю, когда async расширяется, здесь требуется вставить Either , потому что мы либо переходим в состояние, либо нет.

_Вы можете найти здесь актуальный код, если хотите.

@Pzixel вам не понадобится Either внутри async функций, пока вы await фьючерсы, тогда преобразование кода, которое выполняет async , скроет эти два типы внутри и представляют компилятору единственный возвращаемый тип.

@Pzixel Кроме того, я (лично) надеюсь, что Either не будет введен с этим RFC, потому что это будет представлять ограниченную версию https://github.com/rust-lang/rfcs/issues / 2414 (который работает только с двумя типами и только с Future s), поэтому, вероятно, добавит API-интерфейс, если общее решение когда-либо будет объединено - и, как упоминал @ Nemo157, это не похоже на чрезвычайную ситуацию для есть Either прямо сейчас :)

@Ekleog, конечно, меня просто поразила идея, что у меня на самом деле есть тонны either в моем существующем асинхронном коде, и я действительно хотел бы избавиться от них. Затем я вспомнил свое замешательство, когда я потратил ~ полчаса, пока не понял, что он не компилируется, потому что у меня был ящик either в зависимостях (будущие ошибки довольно сложно понять, поэтому это заняло довольно много времени). Вот почему я написал комментарий, просто чтобы убедиться, что эта проблема каким-то образом решена.

Конечно, это не относится только к async/await , это более общая вещь, поэтому она заслуживает отдельного RFC. Я только хотел подчеркнуть, что либо futures должен знать о either либо наоборот (для правильной реализации IntoFuture ).

@Pzixel Either экспортированный ящиком фьючерсов, является реэкспортом из ящика either . futures crate 0.3 не может реализовать Future для Either из-за правил для сирот. Весьма вероятно, что мы собираемся также удалить импы Stream и Sink для Either для согласованности и предложить вместо них альтернативу (обсуждается здесь ). Кроме того, ящик either может реализовать сам Future , Stream и Sink , вероятно, под флагом функции.

При этом, как уже упоминал @ Nemo157 , при работе с фьючерсами лучше просто использовать асинхронные функции вместо Either .

Материал async : Type { .. } теперь предлагается в https://github.com/rust-lang/rfcs/pull/2522.

Реализованы ли функции async / await, автоматически реализующие Send ?

Похоже, что следующая асинхронная функция не (пока?) Send :

pub async fn __receive() -> ()
{
    let mut chan: futures::channel::mpsc::Receiver<Box<Send + 'static>> = None.unwrap();

    await!(chan.next());
}

Ссылка на полный репродуктор (который не компилируется на игровой площадке из-за отсутствия futures-0.3 , я думаю) здесь .

Кроме того, исследуя эту проблему, я наткнулся на https://github.com/rust-lang/rust/issues/53249, который, я думаю, должен быть добавлен в список отслеживания самого верхнего сообщения :)

Вот площадка, показывающая, что функции async / await, реализующие Send _should_, работают. Раскомментирование версии Rc правильно определяет, что эта функция не является Send . Я могу немного взглянуть на ваш конкретный пример (на этой машине нет компилятора Rust: немного_frowning_face :), чтобы попытаться понять, почему он не работает.

@Ekleog std::mpsc::Receiver - это не Sync , а написанное вами async fn содержит ссылку на него. Ссылки на !Sync items - это !Send .

@cramertj Хм ... но разве я не владею собственным mpsc::Receiver , который должен быть Send если его общий тип - Send ? (кроме того, это не std::mpsc::Receiver а futures::channel::mpsc::Receiver , что тоже Sync если тип Send , извините, что не заметил mpsc::Receiver псевдоним был неоднозначным!)

@ Nemo157 Спасибо! Я открыл https://github.com/rust-lang/rust/issues/53259, чтобы избежать лишнего шума по этому вопросу :)

Вопрос о том, разрешают ли и как async блоки ? и другой поток управления, может потребоваться некоторое взаимодействие с try блоками (например, try async { .. } чтобы разрешить ? без путаницы, подобной return ?).

Это означает, что механизму указания типа блока async может потребоваться взаимодействие с механизмом указания типа блока try . Я оставил комментарий к RFC синтаксиса приписывания: https://github.com/rust-lang/rfcs/pull/2522#issuecomment -412577175

Просто нажмите то, что сначала я подумал, это проблема futures-rs , но оказалось, что это может быть проблема async / await, так что вот она: https://github.com/rust-lang-nursery/ futures-rs / issues / 1199 # issuecomment -413089012

Один новый выпуск: https://github.com/rust-lang/rust/issues/53447

Как обсуждалось несколько дней назад в Discord, await еще не зарезервировано в качестве ключевого слова. Очень важно получить эту оговорку (и добавить в ключевое слово lint издания 2018) до выпуска 2018 года. Это немного сложная оговорка, поскольку мы хотим и дальше использовать синтаксис макроса.

Будет ли у Future / Task API способ порождать локальные фьючерсы?
Вижу, что есть SpawnLocalObjError , но вроде не используется.

@panicbit В рабочей группе мы сейчас обсуждаем, имеет ли смысл вообще включать функцию нереста в контекст. https://github.com/rust-lang-nursery/wg-net/issues/56

( SpawnLocalObjError не совсем неиспользуется: его использует LocalPool ящика фьючерсов. Однако вы правы, что ничто в libcore не использует его)

@withoutboats Я заметил, что некоторые ссылки в описании проблемы устарели. В частности, https://github.com/rust-lang/rfcs/pull/2418 закрыт, а https://github.com/rust-lang-nursery/futures-rs/issues/1199 перемещен в https: / /github.com/rust-lang/rust/issues/53548

NB. Название этой проблемы отслеживания - async / await, но она также назначена API задач! API задачи в настоящее время ожидает стабилизации RFC: https://github.com/rust-lang/rfcs/pull/2592

Есть ли шанс сделать ключевые слова многоразовыми для альтернативных реализаций async? в настоящее время он создает будущее, но это своего рода упущенная возможность сделать асинхронный режим на основе push более удобным в использовании.

@aep Можно легко преобразовать систему, основанную на push, в систему Future, основанную на pull, используя oneshot::channel .

Например, обещания JavaScript основаны на push, поэтому stdweb использует oneshot::channel для преобразования обещаний JavaScript в Rust Futures . Он также использует oneshot::channel для некоторых других API обратного вызова на основе push, например setTimeout .

Из-за модели памяти Rust Futures, основанные на push, имеют дополнительные затраты на производительность по сравнению с pull . Так что лучше оплачивать эту стоимость производительности только тогда, когда это необходимо (например, используя oneshot::channel ), вместо того, чтобы вся система Future была основана на push.

Сказав это, я не являюсь частью основной или языковой команды, поэтому ничто из того, что я говорю, не имеет авторитета. Это просто мое личное мнение.

на самом деле это наоборот в коде с ограниченными ресурсами. У моделей pull есть штраф, потому что вам нужен ресурс внутри объекта, который вытаскивают, вместо того, чтобы передавать следующее готовое значение через стек ожидающих функций. Дизайн futures.rs просто слишком дорог для чего-либо, близкого к оборудованию, например, сетевых коммутаторов (мой вариант использования) или средств визуализации игр.

Однако в этом случае все, что нам нужно, это заставить async генерировать что-то вроде Generator. Как я уже сказал, я думаю, что async и генераторы на самом деле одно и то же, если вы достаточно абстрагируете их, вместо того, чтобы связывать два ключевых слова с одной библиотекой.

Однако в этом случае все, что нам нужно, это заставить async генерировать что-то вроде Generator.

async на этом этапе буквально представляет собой минимальную оболочку вокруг литерала генератора. Мне трудно понять, как генераторы помогают с асинхронным вводом-выводом на основе push, разве вам не нужно для них преобразование CPS?

Не могли бы вы уточнить, что вы имеете в виду под «вам нужны ресурсы внутри того, что тянут?» Я не уверен, зачем вам это нужно, или чем «подача следующего готового значения через стек ожидающих функций» отличается от poll() .

У меня сложилось впечатление, что фьючерсы на основе push были более дорогими (и поэтому их труднее было использовать в ограниченных условиях). Разрешение присоединения произвольных обратных вызовов к будущему требует некоторой формы косвенного обращения, обычно выделения кучи, поэтому вместо однократного выделения корневого будущего вы выделяете на каждом комбинаторе. И отмена также становится более сложной из-за проблем с безопасностью потоков, поэтому вы либо не поддерживаете ее, либо требуете, чтобы все завершения обратного вызова использовали атомарные операции, чтобы избежать гонок. Насколько я могу судить, все это значительно усложняет оптимизацию фреймворка.

разве вам не нужно для этого преобразование CPS?

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

вам нужны ресурсы внутри того, что тянут?

это мой ужасный способ сказать, что двойное изменение порядка асинхронной работы имеет свои затраты. Т.е. один раз с железа на фьючерсы и обратно по каналам. Вам нужно иметь при себе целую кучу вещей, которые не имеют преимуществ в почти аппаратном коде.

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

Разрешение привязки произвольных обратных вызовов к будущему требует некоторой косвенной адресации.

да, я понимаю, что обратные вызовы дороги в некоторых средах (не в моей, где скорость выполнения не имеет значения, но у меня общая память 1 МБ, поэтому futures.rs даже не помещается во флэш), однако вам вообще не нужна динамическая отправка, когда у вас есть что-то вроде продолжения (что наполовину реализует нынешняя концепция генератора).

И отмена также становится более сложной из-за безопасности потоков.

я думаю, мы здесь что-то путаем. Я не выступаю за обратные вызовы. Продолжения могут быть статичными стеками. Например, то, что мы реализовали на глиняном языке, - это просто шаблон генератора, который вы можете использовать для нажатия или вытягивания. то есть:

async fn add (a: u32) -> u32 {
    let b = await
    a + b
}

add(3).continue(2) == 5

Думаю, я могу просто продолжить делать это с помощью макроса, но я чувствую, что это упущенная возможность здесь тратить ключевое слово языка на одну конкретную концепцию.

не в моем, где скорость выполнения не имеет значения, но у меня общий объем памяти 1 МБ, поэтому futures.rs даже не помещается на флеш-память

Я почти уверен, что текущие фьючерсы предназначены для работы в средах с ограниченным объемом памяти. Что именно занимает так много места?

Изменить: эта программа занимает 295 КБ дискового пространства при компиляции - отпустите на моем MacBook (базовый привет мир занимает 273 КБ):

use futures::{executor::LocalPool, future};

fn main() {
    let mut pool = LocalPool::new();
    let hello = pool.run_until(future::ready("Hello, world!"));
    println!("{}", hello);
}

не в моем, где скорость выполнения не имеет значения, но у меня общий объем памяти 1 МБ, поэтому futures.rs даже не помещается на флеш-память

Я почти уверен, что текущие фьючерсы предназначены для работы в средах с ограниченным объемом памяти. Что именно занимает так много места?

И что вы имеете в виду под памятью? Я запускал текущий код на основе async / await на устройствах с 128 КБ флэш-памяти / 16 КБ оперативной памяти. В настоящее время определенно существуют проблемы с использованием памяти с async / await, но в основном это проблемы с реализацией и могут быть улучшены путем добавления некоторых дополнительных оптимизаций (например, https://github.com/rust-lang/rust/issues/52924).

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

Почему? Это все еще не похоже ни на что, к чему вас подталкивает будущее. Вы можете так же легко вызвать poll как и механизм на основе push.

И что вы имеете в виду под памятью?

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

Я хочу сказать, что ключевое слово async можно будет использовать в будущем, если оно будет выполнено правильно. Я хочу продолжения, но, возможно, другие люди придут в голову даже лучше.

Вы можете так же легко вызвать опрос, как и механизм на основе push.

Да, это имело бы смысл, если бы в Future: poll были аргументы вызова. Их не может быть, потому что опрос должен быть абстрактным. Вместо этого я предлагаю создать продолжение из ключевого слова async и использовать Future для любого продолжения с нулевыми аргументами.

Это простое изменение с минимальными усилиями, которое не добавляет стоимости фьючерсам, но позволяет повторно использовать ключевые слова, которые в настоящее время предназначены исключительно для одной библиотеки.

Но продолжения, конечно, можно реализовать и с помощью препроцессора, что мы и собираемся сделать. К сожалению, десахар может быть только укупоркой, а это дороже, чем полноценное продолжение.

@aep Как сделать возможным повторное использование ключевых слов ( async и await )?

@Centril, моим наивным быстрым

Это похоже на пиар из 10 строк. Но у меня нет терпения бороться с пчелиным ульем из-за этого, поэтому я просто создам предварительную процедуру для удаления сахара для другого ключевого слова.

Я не следил за асинхронностью, поэтому извиняюсь, если это обсуждалось раньше / где-то еще, но каков план (реализации) для поддержки async / await в no_std ?

AFAICT текущая реализация использует TLS для передачи Waker, но нет поддержки TLS (или потока) в no_std / core . Я слышал от @alexcrichton, что можно было бы избавиться от TLS, если / когда Generator.resume получит поддержку аргументов.

Реализуется ли план по блокировке стабилизации async / await при поддержке no_std ? Или мы уверены, что поддержка no_std может быть добавлена ​​без изменения каких-либо частей, которые будут стабилизированы для отправки std async / await в стабильную версию?

@japaric poll теперь явно принимает контекст. AFAIK, TLS больше не требуется.

https://doc.rust-lang.org/nightly/std/future/trait.Future.html#tymethod.poll

Изменить: не актуально для async / await, только для фьючерсов.

[...] уверены ли мы, что поддержка no_std может быть добавлена ​​без изменения каких-либо частей, которые будут стабилизированы для отправки std async / await в стабильную версию?

Я так считаю. Соответствующие части - это функции в std::future , все они скрыты за дополнительной нестабильной функцией gen_future , которая никогда не будет стабилизирована. Преобразование async использует set_task_waker для сохранения waker в TLS, затем await! использует poll_with_tls_waker для получения доступа к нему. Если генераторы получают поддержку аргумента возобновления, то вместо этого преобразование async может передать пробуждающего агента в качестве аргумента возобновления, а await! может прочитать его из аргумента.

РЕДАКТИРОВАТЬ: Даже без аргументов генератора я считаю, что это также можно сделать с помощью немного более сложного кода в асинхронном преобразовании. Я лично хотел бы, чтобы аргументы генератора добавлялись для других вариантов использования, но я почти уверен, что удаление требования TLS с / без них будет возможно.

@japaric Та же лодка. Даже если кто-то заставил фьючерсы работать на встроенных, это очень рискованно, так как все это Tier3.

Я придумал уродливый хак, который требует гораздо меньше работы, чем исправление async: weave in an Arcчерез стек Генераторов.

  1. см. аргумент «Опрос» https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 его дуга
  2. регистрация чего-либо в опросе в строке 87
  3. yield для создания точки продолжения в строке 92
  4. вызвать генератор из генератора для создания стека более высокого уровня в строке 207
  5. наконец, выполнение всего стека путем передачи среды выполнения в строке 215

В идеале они просто понизили бы асинхронность до «чистого» стека замыкания, а не до Future, чтобы вам не потребовались какие-либо предположения времени выполнения, и вы могли бы затем протолкнуть нечистую среду в качестве аргумента в корень.

Я был на полпути к реализации этого

https://twitter.com/arvidep/status/1067383652206690307

но как бы бессмысленно идти до конца, если я единственный, кто этого хочет.

И я не мог перестать думать о том, возможен ли async / await без TLS без аргументов генератора, поэтому я реализовал no_std proc-macro на основе пары макросов async_block! / await! используя только локальные переменные.

Это определенно требует гораздо более тонких гарантий безопасности, чем текущее решение на основе TLS или решение на основе аргументов генератора (по крайней мере, когда вы просто предполагаете, что базовые аргументы генератора верны), но я почти уверен, что это нормально (если никто использует довольно большую гигиеническую дыру, которую я не мог найти, это не было бы проблемой для реализации в компиляторе, поскольку он может использовать безымянные идентификаторы gensym для связи между асинхронным преобразованием и макросом ожидания).

Я только что понял, что нет никакого упоминания о перемещении await! из std в core в OP, возможно, # 56767 можно было бы добавить в список проблем, которые нужно решить перед стабилизацией, чтобы отслеживать это.

@ Nemo157 Поскольку await! не ожидается, это в любом случае не является блокировщиком.

@Centril Я не знаю, кто тебе сказал, что await! не ожидается стабилизации ...: wink:

@cramertj Он имел в виду версию макроса, а не версию ключевого слова, как мне кажется ...

@ crlf0710 Я тоже :)

@cramertj Разве мы не хотим удалить макрос, потому что в настоящее время в компиляторе есть некрасивый взлом, который делает возможным существование как await и await! ? Если мы стабилизируем макрос, мы никогда не сможем его удалить.

@stjepang Меня действительно не слишком заботит синтаксис await! в любом направлении, за исключением общего предпочтения постфиксных обозначений и неприязни к двусмысленности и непроизносимых / неподходящих для Google символов. Насколько мне известно, текущие предложения (с ? для уточнения приоритета):

  • await!(x)? (то, что у нас есть сегодня)
  • await x? ( await связывает более жестко, чем ? , по-прежнему используется префиксная нотация, требуются скобки для цепочки методов)
  • await {x}? (то же, что и выше, но временно требуется {} для устранения неоднозначности)
  • await? x ( await связывает менее жестко, по-прежнему используется префиксная нотация, требуются скобки для цепочки методов)
  • x.await? (похоже на доступ к полю)
  • x# / x~ / и т. Д. (какой-то символ)
  • x.await!()? (postfix-macro-style, @withoutboats и я думаю, что, возможно, другие не поклонники постфиксных макросов, потому что они ожидают, что . разрешит отправку на основе типов, чего не было бы для макросов postfix )

Я думаю, что лучший способ доставки - это приземлить await!(x) , без ключевого слова await и, в конечном итоге, когда-нибудь продать людям удобство постфиксных макросов, что позволит нам добавить x.await!() . У других разные мнения;)

Я очень слабо слежу за этим вопросом, но вот мое мнение:

Лично мне нравится макрос await! каком он есть и как он описан здесь: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Это не какая-то магия или новый синтаксис, просто обычный макрос. В конце концов, меньше значит лучше.

Опять же, я также предпочел try! , так как Try все еще не стабилизируется. Однако await!(x)? - это достойный компромисс между сахаром и действиями с очевидными именами, и я думаю, что он работает хорошо. Кроме того, он потенциально может быть заменен каким-либо другим макросом в сторонней библиотеке для обработки дополнительных функций, таких как отслеживание отладки.

Между тем async / yield - это «просто» синтаксический сахар для генераторов. Это напоминает мне дни, когда JavaScript получал поддержку async / await, и у вас были такие проекты, как Babel и Regenerator, которые транслировали асинхронный код для использования генераторов и Promises / Futures для асинхронных операций, по сути, так же, как мы.

Имейте в виду, что в конечном итоге мы захотим, чтобы async и генераторы были отдельными функциями, потенциально даже компонуемыми друг с другом (создавая Stream ). Оставить await! в качестве макроса, который просто понижается до yield , не является постоянным решением.

Уезжаем ждать! поскольку макрос, который просто снижает урожайность, не является постоянным решением.

Невозможно постоянно видеть для пользователя то, что он понижается до yield , но это, безусловно, может быть реализовано таким образом. Даже если у вас есть async + generators = Stream вы все равно можете использовать, например, yield Poll::Pending; vs. yield Poll::Ready(next_value) .

Имейте в виду, что в конечном итоге мы захотим, чтобы асинхронность и генераторы были разными функциями.

Разве асинхронность и генераторы не являются отдельными функциями? Связано, конечно, но сравнивая это снова с тем, как это делает JavaScript, я всегда думал, что async будет построен поверх генераторов; что единственное отличие состоит в том, что асинхронная функция вернет и даст Future s в отличие от любого обычного значения. Исполнитель должен будет оценить и дождаться выполнения асинхронной функции. Плюс кое-что еще на всю жизнь, я не уверен.

Фактически, я однажды написал библиотеку именно об этом, рекурсивно оценивая как асинхронные функции, так и функции генератора, возвращающие Promises / Futures.

@cramertj Это не может быть реализовано таким образом, если это два разных «эффекта». Здесь есть некоторые обсуждения: https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202. Мы не хотим yield Poll::Ready(next_value) , мы хотим yield next_value и иметь await s где-либо еще в той же функции.

@rpjohnst

Мы не хотим выдавать Poll :: Ready (next_value), мы хотим выдавать next_value и иметь ожидания в другом месте той же функции.

Да, конечно, это то, что может показаться пользователю, но с точки зрения обессахаривания вам просто нужно обернуть yield s в Poll::Ready и добавить Poll::Pending к yield сгенерированный из await! . Синтаксически для конечных пользователей они представляются как отдельные функции, но они по-прежнему могут совместно использовать реализацию в компиляторе.

@cramertj Также этот:

  • await? x

@novacrazy Да, это разные функции, но их следует компоновать вместе.

И действительно, в JavaScript их можно компоновать:

https://thenewstack.io/whats-coming-up-in-javascript-2018-async-generators-better-regex/

«Асинхронные генераторы и итераторы - это то, что вы получаете, когда комбинируете асинхронную функцию и итератор, так что это похоже на асинхронный генератор, который вы можете ждать, или асинхронную функцию, из которой вы можете получить результат», - пояснил он. Раньше ECMAScript позволял вам писать функцию, которую вы могли сдать или подождать, но не то и другое одновременно. «Это действительно удобно для использования потоков, которые становятся все более и более частью веб-платформы, особенно с объектом Fetch, предоставляющим потоки».

Асинхронный итератор похож на шаблон Observable, но более гибкий. «Observable - это выталкивающая модель; Как только вы подпишетесь на него, вы будете получать события и уведомления на полной скорости, независимо от того, готовы вы или нет, поэтому вам нужно реализовать стратегии буферизации или выборки, чтобы справиться с болтливостью », - объяснил Терлсон. Асинхронный итератор - это двухтактная модель - вы запрашиваете значение, и оно отправляется вам, - которая лучше подходит для таких вещей, как примитивы сетевого ввода-вывода.

@Centril ok, открыто # 56974, это достаточно правильно, чтобы добавить его в качестве нерешенного вопроса в OP?


Я действительно не хочу снова вдаваться в синтаксис await , но я должен ответить хотя бы на один пункт:

Лично мне нравится макрос await! как он есть и как он описан здесь: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

Обратите внимание, что я также сказал, что не верю, что макрос может оставаться макросом, реализованным в библиотеке (игнорируя, будет ли он продолжать отображаться как макрос для пользователей), чтобы подробнее остановиться на причинах:

  1. Скрытие базовой реализации, поскольку одна из нерешенных проблем говорит о том, что в настоящее время вы можете создать генератор, используя || await!() .
  2. Поддержка асинхронных генераторов, как упоминает @cramertj, требует различения yield добавленных await и других yield написанных пользователем. Это _ можно_ сделать на этапе перед расширением макроса, _ если_ пользователи никогда не хотели yield внутри макросов, но есть очень полезные конструкции yield -in-macro, такие как yield_from! . С ограничением, что yield s в макросах должен поддерживаться, это требует, чтобы await! был как минимум встроенным макросом (если не фактическим синтаксисом).
  3. Поддержка async fn на no_std . Я знаю два способа реализовать это, оба требуют async fn -created- Future и await чтобы поделиться идентификатором, в котором хранится waker. Единственный способ, которым я можно увидеть, что гигиенически безопасный идентификатор используется в этих двух местах, если оба они реализованы в компиляторе.

Я думаю, здесь есть небольшая путаница - никогда не было намерения, чтобы await! было открыто расширяемо до оболочки вокруг вызовов yield . В будущем синтаксис await! подобный макросу, будет зависеть от реализации, не отличающейся от реализации поддерживаемых компилятором compile_error! , assert! , format_args! и т. Д. и сможет обессахаривать другой код в зависимости от контекста.

Единственный важный момент, который необходимо понять, - это то, что нет значительной семантической разницы между любым из предложенных синтаксисов - это просто поверхностный синтаксис.

Я бы написал альтернативу для решения синтаксиса await .

Прежде всего, мне нравится идея использовать await в качестве постфиксного оператора. Но, как уже отмечалось, expression.await слишком похоже на поле.

Итак, мое предложение - expression awaited . Недостатком здесь является то, что awaited еще не сохранено в качестве ключевого слова, но оно более естественно в английском языке, и все же нет таких выражений (я имею в виду, грамматические формы, такие как expression [token] ), действительные в Rust. прямо сейчас, так что это можно оправдать.

Затем мы можем написать expression? awaited для ожидания Result<Future,_> и expression awaited? для ожидания Future<Item=Result<_,_>> .

@earthengine

Хотя меня не интересует ключевое слово awaited , я думаю, вы кое-что поняли.

Ключевой вывод здесь: yield и await похожи на return и ? .

return x возвращает значение x , а x? разворачивает результат x , возвращая раньше, если это Err .
yield x дает значение x , а x awaited ожидает будущего x , возвращаясь раньше, если это Pending .

В этом есть хорошая симметрия. Возможно, await действительно должен быть оператором постфикса.

let x = x.do_something() await.do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Я не поклонник ожидаемого постфиксного синтаксиса по той причине, по которой только что показал await! / await . В нем нет простоты ? , и у нас заканчиваются символы для использования в постфиксном операторе ...

Лично я все еще поддерживаю await! по причинам, которые я описал ранее. Он чувствует себя ржавым и серьезным.

Это снижает общую читаемость, особенно для длинных выражений или связанных выражений.

В стандартах Rustfmt пример должен быть написан

let x = x.do_something() await
         .do_another_thing() await;
let x = x.foo(|| ...)
         .bar(|| ...)
         .baz() await;

Я с трудом понимаю, как это влияет на читабельность.

Мне тоже нравится postfix await. Я думаю, что использование пространства было бы необычным и могло бы нарушить умственную группировку. Однако я действительно думаю, что .await!() будет хорошо сочетаться, с ? подходящим либо до, либо после, а ! позволит взаимодействовать с потоком управления.

(Для этого не требуется полностью общий механизм постфиксных макросов; компилятор может использовать особый случай .await!() .)

Сначала мне очень не нравился постфикс await (без . или () ), потому что он выглядит довольно странно - люди, пришедшие с других языков, получат хороший смешок над нашими расход точно. К этой цене мы должны отнестись серьезно. Однако x await явно не является вызовом функции или доступом к полю ( x.await / x.await() / await(x) все имеют эту проблему) и меньше фанков вопросы приоритета. Этот синтаксис явно разрешит ? и приоритет доступа к методу, например, foo await? и foo? await оба имеют четкий порядок приоритета для меня, как и foo await?.x и foo await?.y (не отрицая, что они выглядят странно, только утверждая, что приоритет ясен).

я тоже так думаю

stream.for_each(async |item| {
    ...
}) await;

читается лучше, чем

await!(stream.for_each(async |item| {
    ...
});

В общем, я бы поддержал это.

@joshtriplett RE .await!() мы должны поговорить отдельно - Я изначально тоже был за это, но я не думаю, что мы должны это делать, если мы не можем также получить постфиксные макросы в целом, и я думаю по отношению к ним много постоянного сопротивления (по довольно хорошей, хотя и неудачной причине), и мне бы очень хотелось, чтобы это не блокировало стабилизацию await .

Почему не оба?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Я действительно вижу сейчас привлекательность постфикса, и в некоторых случаях мне кажется, что он мне больше нравится. Особенно с указанным выше читом, который настолько прост, что даже не обязательно должен быть предоставлен самим Rust.

Итак, +1 для постфикса.

Я действительно думаю, что у нас также должна быть префиксная функция в дополнение к постфиксной версии.

Что касается специфики синтаксиса постфикса, я не пытаюсь сказать, что .await!() - единственный жизнеспособный синтаксис постфикса; Я просто не фанат постфикса await с пробелом в начале.

Он выглядит приемлемым (хотя и необычным), когда вы форматируете его по одному оператору в строке, но гораздо менее разумным, когда вы форматируете простые операторы в одной строке.

Для тех, кто не любит постфиксные операторы ключевых слов, мы можем определить правильный символический оператор для await .

Прямо сейчас у нас как бы закончились операторы в простых символах ASCII для постфиксного оператора. Однако как насчет

let x = do_something()⌛.do_somthing_else()⌛;

Если нам действительно нужен простой ASCII, я придумал (вдохновленный формой выше)

let x = do_something()><.do_somthing_else()><;

или (имитация формы в горизонтальном положении)

let x = do_something()>=<.do_somthing_else()>=<;

Другая идея - сделать структуру await скобкой.

let x = >do_something()<.>do_something_else()<;

Все эти решения ASCII имеют одну и ту же временную проблему, что <..> уже чрезмерно используется, и у нас есть проблемы с синтаксическим анализом с < и > . Однако для этого могут быть лучше >< или >=< поскольку они не требуют места внутри оператора и не открывают < s в текущей позиции.


Для тех, кто просто не любит пробел между ними, но подходит для операторов ключевого слова postfix, как насчет использования дефисов:

let x = do_something()-await.do_something_else()-await;

Что касается множества разных способов написания одного и того же кода, мне лично это не нравится. Основная причина того, что новичкам намного сложнее правильно понять, что это за правильный путь или смысл. Вторая причина заключается в том, что у нас будет много разных проектов, использующих другой синтаксис, и будет труднее переключаться между ними и читать их (особенно для новичков в ржавчине). Я считаю, что другой синтаксис следует применять только в том случае, если действительно есть разница, и это дает некоторые преимущества. Много сахара в коде просто усложняет изучение языка и работу с ним.

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

Более того, с точки зрения макроса await! нет никакого способа помешать пользователю изобретать свои собственные макросы, чтобы делать это иначе, и Rust предназначен для включения этого. Следовательно, «иметь много разных способов сделать одно и то же» неизбежно.

Я думаю, что этот простой тупой макрос, который я показал, демонстрирует, что независимо от того, что мы делаем, пользователь в любом случае будет делать все, что хочет. Ключевое слово, будь то префикс или постфикс, может быть преобразовано либо в макрос префикса, подобный функции, либо, предположительно, в макрос, подобный методу постфикса, если таковой существует. Даже если мы выбрали макросы, подобные функциям или методам, для await , они могут быть инвертированы с помощью еще одного макроса. На самом деле это не имеет значения.

Поэтому нам следует сосредоточиться на гибкости и форматировании. Предложите решение, которое наиболее легко восполнило бы все эти возможности.

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

Я пришел к выводу, что префиксное ключевое слово await является лучшим синтаксисом по умолчанию на данный момент, возможно, с обычным ящиком, предоставляющим пользователям макрос await! , похожий на функцию, а в будущем - похожий на постфиксный метод .await!() макрос.

@novacrazy

Кроме того, хотя за это короткое время я привязался к синтаксису ключевых слов постфикса, await должно отражать все, что было решено для yield с генераторами, что, вероятно, является ключевым словом префикса.

Выражение yield 42 имеет тип ! , тогда как foo.await имеет тип T где foo: impl Future<Output = T> . @stjepang проводит здесь правильную аналогию с ? и return . await - это не yield .

Почему не оба?

macro_rules! await {
    ($e:expr) => {{$e await}}
}

Вам нужно будет назвать макрос как-нибудь иначе, потому что await должно оставаться истинным ключевым словом.


По ряду причин я против префикса await и тем более формы блока await { ... } .

Во-первых, есть проблемы с приоритетом с await expr? где постоянный приоритет равен await (expr?) но вы хотите (await expr)? . В качестве решения проблем с приоритетом некоторые предлагали await? expr в дополнение к await expr . Это влечет за собой await? как единицу и специальный кожух; это кажется необоснованным, пустой тратой нашего бюджета сложности и признаком того, что await expr имеет серьезные проблемы.

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

Еще хуже было бы await { ... } . Этот синтаксис при последовательном форматировании в соответствии с rustfmt превратился бы в:

    let x = await { // by analogy with `loop`
        foo.bar.baz.other_thing()
    };

Это было бы неэргономично и значительно увеличило бы длину функций по вертикали.


Вместо этого я думаю, что ожидание, например ? , должно быть постфиксным, поскольку это соответствует экосистеме Rust, которая сосредоточена вокруг цепочки методов. Было упомянуто несколько постфиксных синтаксисов; Я пройдусь по некоторым из них:

  1. foo.await!() - это решение макроса постфикса . Хотя я решительно поддерживаю макросы postfix, я согласен с @cramertj в https://github.com/rust-lang/rust/issues/50547#issuecomment -454225040, что мы не должны этого делать, если мы также не обязуемся использовать postfix. макросы в целом. Я также думаю, что использование постфиксного макроса таким образом дает довольно не первоклассное ощущение; мы должны imo избегать использования синтаксиса макросов в языковых конструкциях.

  2. foo await - Это не так уж плохо, это действительно работает как оператор постфикса ( expr op ), но я чувствую, что в этом форматировании чего-то не хватает (т.е. оно кажется "пустым"); Напротив, expr? прикрепляет ? непосредственно к expr ; здесь нет места. Это делает ? визуально привлекательным.

  3. foo.await - Его критиковали за то, что он выглядел как доступ к полю; и это правда. Однако мы должны помнить, что await - это ключевое слово, и поэтому синтаксис будет выделен как таковой. Если вы читаете код Rust в своей среде IDE или аналогично на GitHub, await будет другого цвета или жирности, чем foo . Используя другое ключевое слово, мы можем продемонстрировать это:

    let x = foo.match?;
    

    Обычно поля также являются существительными, тогда как await - это глагол.

    Хотя есть начальный фактор насмешек по поводу foo.await , я думаю, что его следует серьезно рассмотреть как визуально привлекательный, но в то же время читаемый синтаксис.

    В качестве бонуса использование .await дает вам силу точки и автозаполнение, которое точка обычно имеет в IDE (см. Стр. 56). Например, вы можете написать foo. и если foo окажется будущим, await будет отображаться как первый вариант. Это облегчает как эргономику, так и продуктивность разработчиков, поскольку стремление к точке - это то, что многие разработчики приучили к мышечной памяти.

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

  4. foo# - использует сигилу # для ожидания foo . Я думаю, что использование сигилы - хорошая идея, учитывая, что ? также является сигилой и потому, что она делает ожидание легким. В сочетании с ? это будет выглядеть как foo#? - выглядит нормально. Однако # не имеет конкретного обоснования. Скорее, это просто сигилла, которая все еще доступна.

  5. foo@ - Другой символ - @ . В сочетании с ? мы получаем foo@? . Одно из оправданий этой особой сигилы состоит в том, что она выглядит a -ish ( @wait ).

  6. foo! - Наконец, есть ! . В сочетании с ? мы получаем foo!? . К сожалению, в этом есть определенное чувство WTF. Однако ! выглядит как принудительное значение, подходящее для "await". Есть один недостаток в том, что foo!() уже является допустимым вызовом макроса, поэтому для ожидания и вызова функции необходимо написать (foo)!() . Использование foo! в качестве синтаксиса также лишит нас возможности иметь макросы ключевых слов (например, foo! expr ).

Еще одна одиночная сигилла - foo~ . Волну можно понимать как «эхо» или «требует времени». Однако в языке Rust он нигде не используется.

Тильда ~ раньше использовалась для типа, выделенного в куче: https://github.com/rust-lang/rfcs/blob/master/text/0059-remove-tilde.md

Можно ли повторно использовать ? ? Или это слишком много волшебства? Как бы выглядел impl Try for T: Future ?

@parasyte Да, я помню. Но все равно его давно не было.

@jethrogb нет возможности увидеть, что impl Try работает напрямую, ? явно return s - результат Try из текущей функции, а await необходимо yield .

Возможно, ? может иметь особый случай делать что-то еще в контексте генератора, чтобы он мог либо yield или return зависимости от типа выражения, к которому оно применяется. , но я не уверен, насколько это было бы понятно. Также как это будет взаимодействовать с Future<Output=Result<...>> , нужно ли вам let foo = bar()??; чтобы оба выполняли "ожидание", а затем получили вариант Ok из Result ( или ? в генераторах будет основываться на трехстороннем признаке, который может yield , return или разрешить значение с помощью одного приложения)?

Последнее замечание в скобках на самом деле заставляет меня думать, что с ним можно работать, нажмите, чтобы увидеть быстрый набросок
enum GenOp<T, U, E> { Break(T), Yield(U), Error(E) }

trait TryGen {
    type Ok;
    type Yield;
    type Error;

    fn into_result(self) -> GenOp<Self::Ok, Self::Yield, Self::Error>;
}
с `foo?` внутри генератора, расширяющимся до чего-то вроде (хотя у этого есть проблема владения, а также необходимо закрепить результат `foo` в стеке)
loop {
    match TryGen::into_result(foo) {
        GenOp::Break(val) => break val,
        GenOp::Yield(val) => yield val,
        GenOp::Return(val) => return Try::from_error(val.into()),
    }
}

К сожалению, я не вижу, как обрабатывать переменную контекста waker в такой схеме, возможно, если бы ? были в специальном регистре для async вместо генераторов, но если это будет особенным -cased здесь было бы неплохо, если бы его можно было использовать для других сценариев использования генераторов.

У меня была такая же мысль относительно повторного использования ? качестве @jethrogb.

@ Nemo157

нет никакого способа, чтобы я мог видеть, что impl Try напрямую работает, ? явно return s результат Try из текущей функции, в то время как await требует yield .

Возможно, мне не хватает деталей по ? и трейту Try , но где / почему это явно? И разве return в асинхронном закрытии по сути не то же самое, что yield , а просто переход другого состояния?

Может быть, ? может иметь специальный корпус, чтобы делать что-то еще в контексте генератора, чтобы он мог либо yield или return зависимости от типа выражения, к которому оно применяется. , но я не уверен, насколько это было бы понятно.

Я не понимаю, почему это должно сбивать с толку. Если вы думаете о ? как о «продолжаться или расходиться», то это кажется естественным, ИМХО. Конечно, может помочь изменение признака Try для использования других имен для связанных типов возвращаемых значений.

Также как это будет взаимодействовать с Future<Output=Result<...>> , если бы вам пришлось let foo = bar()?? ;

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

К сожалению, я не понимаю, как обрабатывать контекстную переменную waker в такой схеме, может быть, если? были в специальном корпусе для async вместо генераторов, но если здесь он будет в специальном корпусе, было бы неплохо, если бы его можно было использовать для других сценариев использования генераторов.

Я не понимаю этой части. Не могли бы вы уточнить?

@jethrogb @rolandsteiner Структура может реализовывать как Try и Future . В этом случае, какой из них следует развернуть ? ?

@jethrogb @rolandsteiner Структура может реализовывать как Try, так и Future. В таком случае, какой из них? развернуть?

Нет, это не могло быть из-за одеяла, имплантированного в «Попытку» для T: Future.

Почему никто не говорит о явной конструкции и неявном предложении

но это все просто затенение велосипеда, я думаю, нам стоит довольствоваться простым синтаксисом макросов await!(my_future) по крайней мере пока

но это все просто затенение велосипеда, я думаю, нам стоит довольствоваться простым синтаксисом макросов await!(my_future) по крайней мере пока

Нет, это не «просто» сбрасывание байков, как будто это что-то банальное и незначительное. Написание префикса или постфикса в await существенно влияет на то, как пишется асинхронный код. цепочки методов и насколько она удобна в компоновке. Стабилизация на await!(future) также влечет за собой отказ от await в качестве ключевого слова, что делает невозможным использование await в качестве ключевого слова в будущем. «По крайней мере, сейчас» предполагает, что мы сможем найти лучший синтаксис позже, и игнорируем технический долг, который это влечет за собой. Я против сознательного введения долга за синтаксис, который предполагается заменить позже.

Стабилизация на await! (Будущее) также влечет за собой отказ от использования await в качестве ключевого слова, что делает невозможным использование await в качестве ключевого слова в будущем.

мы могли бы сделать его ключевым словом в следующей эпохе, потребовав необработанного синтаксиса идентификатора для макроса, точно так же, как мы сделали с try .

@rolandsteiner

И разве return в асинхронном закрытии по сути не то же самое, что yield , а просто переход другого состояния?

yield не существует в асинхронном закрытии, это операция, введенная во время понижения синтаксиса async / await до генераторов / yield . В текущем синтаксисе генератора yield сильно отличается от return , если расширение ? выполняется до преобразования генератора, тогда я не знаю, как он узнает, когда вставлять return или yield .

Если вы хотите дождаться результата, а также досрочно выйти из-за ошибки, тогда это будет логическое выражение, да.

Это могло бы быть логично, но мне кажется недостатком, что многие (большинство?) Случаев, когда вы пишете асинхронные функции, будут заполнены двойным ?? чтобы справиться с ошибками ввода-вывода.

К сожалению, я не понимаю, как обрабатывать контекстную переменную waker ...

Я не понимаю этой части. Не могли бы вы уточнить?

Асинхронное преобразование принимает переменную waker в сгенерированной функции Future::poll , которая затем должна быть передана в преобразованную операцию ожидания. В настоящее время это обрабатывается с помощью переменной TLS, предоставляемой std которая обе преобразует ссылку, если ? вместо этого обрабатывались как точка повторного выхода _ на уровне генераторов_, тогда асинхронное преобразование проигрывает чтобы вставить ссылку на эту переменную.

Я написал в блоге сообщение о синтаксисе await изложением моих предпочтений по состоянию на два месяца назад. Однако он в основном предполагал синтаксис префикса и просто рассматривал проблему приоритета с этой точки зрения. Вот еще несколько дополнительных мыслей:

  • Мое общее мнение таково, что Rust уже реально растянул свой бюджет на незнание. Было бы идеально, если бы синтаксис async / await на поверхностном уровне был как можно более знаком тем, кто работает с JavaScript, Python или C #. С этой точки зрения было бы идеально отклоняться от нормы лишь в незначительной степени. Синтаксисы постфиксов различаются в зависимости от того, насколько далеко они расхождения (например, foo await меньше расхождения, чем некоторые сигилы, такие как foo@ ), но все они более расходятся, чем prefix await.
  • Я также предпочитаю стабилизировать синтаксис, в котором не используется ! . Каждый пользователь, имеющий дело с async / await, будет задаваться вопросом, почему await - это макрос, а не обычная конструкция потока управления, и я считаю, что история здесь будет, по сути, «хорошо, мы не смогли придумать хороший синтаксис, поэтому мы просто остановились на делая его похожим на макрос ". Это не убедительный ответ. Я не думаю, что связи между ! и потоком управления действительно достаточно, чтобы оправдать этот синтаксис: я считаю, что ! имеет довольно специфические средние макрорасходы, а это не так.
  • Я как бы сомневаюсь в пользе postfix await в целом (не совсем, просто вроде ). Я чувствую, что баланс немного отличается от ? , потому что ожидание - более дорогостоящая операция (вы выполняете цикл до его готовности, а не просто переходите и возвращаетесь один раз). Я с подозрением отношусь к коду, который ожидал бы два или три раза в одном выражении; мне кажется нормальным сказать, что их нужно вытащить в свои собственные привязки let. Так что компромисс между try! и ? не так сильно меня привлекает. Но также я был бы открыт для примеров кода, которые, по мнению людей, действительно не следует вытаскивать в группы, и которые более понятны в виде цепочек методов.

Тем не менее, foo await - самый жизнеспособный синтаксис постфикса, который я видел до сих пор:

  • Это относительно знакомо для постфиксного синтаксиса. Все, что вам нужно усвоить, это то, что await идет после выражения, а не до него в Rust, а не значительно отличается от синтаксиса.
  • Это явно решает проблему приоритета, о которой все это шло.
  • Тот факт, что он плохо работает с цепочкой методов, кажется мне почти преимуществом, а не недостатком по причинам, о которых я упоминал ранее. Я мог бы быть более вынужден, если бы у нас были некоторые грамматические правила, предотвращающие foo await.method() только потому, что я действительно чувствую, что метод (бессмысленно) применяется к await , а не к foo (что интересно Я не чувствую этого с foo await? ).

Я все еще склоняюсь к синтаксису префикса, но я думаю, что await - это первый синтаксис постфикса, который, похоже, имеет для меня реальный шанс.

Примечание: всегда можно использовать скобки, чтобы прояснить приоритет:

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Это не совсем идеально, но, учитывая, что он пытается втиснуть многое в одну строку, я думаю, что это разумно.

И, как упоминалось ранее в @earthengine , многострочная версия очень разумна (без лишних скобок):

let x = x.do_something() await
         .do_another_thing() await;

let x = x.foo(|| ...)
         .bar(|| ... )
         .baz() await;
  • Было бы идеально, если бы синтаксис async / await на поверхностном уровне был как можно более знаком тем, кто работает с JavaScript, Python или C #.

В случае с try { .. } мы учли знакомство с другими языками. Однако это был также правильный дизайн с точки зрения внутренней согласованности с Rust. Итак, при всем уважении к этим другим языкам, внутренняя согласованность в Rust кажется более важной, и я не думаю, что синтаксис префиксов подходит Rust ни с точки зрения приоритета, ни с точки зрения структуры API.

  • Я также предпочитаю стабилизировать синтаксис, в котором не используется ! . Каждый пользователь, имеющий дело с async / await, будет задаваться вопросом, почему await - это макрос, а не обычная конструкция потока управления, и я считаю, что история здесь будет, по сути, «хорошо, мы не смогли придумать хороший синтаксис, поэтому мы просто остановились на делая его похожим на макрос ". Это не убедительный ответ.

Я согласен с этим мнением, .await!() не будет выглядеть достаточно первоклассно.

  • Я как бы сомневаюсь в преимуществах postfix await в целом (не совсем, просто _sort of_). Я чувствую, что баланс немного отличается от ? , потому что ожидание - более дорогая операция (вы выполняете цикл, пока он не будет готов, а не просто ветвление и возврат один раз).

Я не понимаю, какое отношение имеет дороговизна к извлечению вещей в привязки let . Цепочки методов могут быть дорогими, а иногда и дорогими. Преимущество привязок let состоит в том, что а) дают достаточно большим частям имя, которое имеет смысл для повышения читабельности, б) возможность ссылаться на одно и то же вычисленное значение более одного раза (например, с помощью &x или когда тип копируемый).

Я с подозрением отношусь к коду, который ожидал бы два или три раза в одном выражении; мне кажется нормальным сказать, что их нужно вытащить в свои собственные привязки let.

Если вы чувствуете, что их следует вытащить в свои собственные привязки let вы все равно можете сделать этот выбор с помощью постфикса await :

let temporary = some_computation() await?;

Для тех, кто не согласен и предпочитает цепочку методов, postfix await дает возможность выбора. Я также думаю, что postfix лучше следует здесь за чтением слева направо и порядком потока данных, поэтому даже если вы извлекаете привязки let я все равно предпочитаю postfix.

Я также не думаю, что вам нужно ждать два или три раза, чтобы postfix await стал полезным. Рассмотрим для примера (это результат rustfmt ):

    let foo = alpha()
        .beta
        .some_other_stuff()
        .await?
        .even_more_stuff()
        .stuff_and_stuff();

Но также я был бы открыт для примеров кода, которые, по мнению людей, действительно не следует вытаскивать в группы, и которые более понятны в виде цепочек методов.

Большая часть кода фуксии, которую я читал, казалась неестественной при извлечении в привязки let и с let binding = await!(...)?; .

  • Это относительно знакомо для постфиксного синтаксиса. Все, что вам нужно усвоить, это то, что await идет после выражения, а не до него в Rust, а не значительно отличается от синтаксиса.

Я предпочитаю здесь foo.await в основном потому, что у вас хорошее автозаполнение и сила точки. Это тоже не кажется таким радикальным. Запись foo.await.method() также проясняет, что .method() применяется к foo.await . Так что это решает эту проблему.

  • Это явно решает проблему приоритета, о которой все это шло.

Нет, дело не только в приоритете. Цепочки методов не менее важны.

  • Тот факт, что он плохо работает с цепочкой методов, кажется мне почти преимуществом, а не недостатком по причинам, о которых я упоминал ранее.

Я не уверен, почему это не работает с цепочкой методов.

Я мог бы быть более вынужден, если бы у нас были некоторые грамматические правила, предотвращающие foo await.method() только потому, что я действительно чувствую, что метод (бессмысленно) применяется к await , а не к foo (что интересно Я не чувствую этого с foo await? ).

Принимая во внимание, что я был бы не принужден к использованию foo await если бы мы ввели преднамеренный дизайн papercut и предотвратили цепочку методов с синтаксисом постфиксного await .

Допуская, что у каждого варианта есть обратная сторона, и что один из них, тем не менее, должен быть выбран ... в foo.await меня беспокоит то, что, даже если мы предположим, что он буквально не будет принят за поле структуры, это по-прежнему похоже на доступ к полю структуры. Смысл доступа к полям состоит в том, что ничего особо важного не происходит - это одна из наименее эффективных операций в Rust. Между тем ожидание - это очень эффективная операция, одна из самых побочных операций (она одновременно выполняет операции ввода-вывода, созданные в Future, и имеет эффекты потока управления). Поэтому, когда я читаю foo.await.method() , мой мозг говорит мне пропустить .await потому что это относительно неинтересно, и я должен использовать внимание и усилия, чтобы вручную преодолеть этот инстинкт.

он по-прежнему _похоже_ на доступ к полю структуры.

@glaebhoerl Вы делаете хорошие .await который имеет другой цвет, чем остальные вещи.

Смысл доступа к полям состоит в том, что ничего особо важного не происходит - это одна из наименее эффективных операций в Rust. Между тем ожидание - это очень эффективная операция, одна из самых побочных операций (она одновременно выполняет операции ввода-вывода, созданные в Future, и имеет эффекты потока управления).

Я полностью согласен с этим. await - это операция потока управления, например break или return , и она должна быть явной. Предлагаемая постфиксная нотация кажется неестественной, как if Python: сравните if c { e1 } else { e2 } с e1 if c else e2 . Вид оператора в конце заставляет задуматься, независимо от выделения синтаксиса.

Я также не понимаю, почему e.await более согласуется с синтаксисом Rust, чем await!(e) или await e . Другого ключевого слова postfix нет, и поскольку одна из идей заключалась в том, чтобы выделить его в парсере, я не думаю, что это доказательство согласованности.

Также упоминается проблема знакомства @withoutboats . Мы можем выбрать необычный и замечательный синтаксис, если у него есть замечательные преимущества. Но есть ли они у постфикса await ?

Подсветка синтаксиса не влияет / недостаточно влияет на то, как она выглядит, и на то, как ваш мозг обрабатывает вещи?

(Хороший вопрос, я уверен, что это окажет какое-то влияние, но трудно угадать, сколько, не попробовав на самом деле (и до замены другого ключевого слова пока не доходит). Пока мы обсуждаем эту тему ... долгое время назад я упоминал, что, по моему мнению, подсветка синтаксиса должна выделять все операторы с эффектами потока управления ( return , break , continue , ? ... и теперь await ) каким-то особенным экстра-отличительным цветом, но я не отвечаю за подсветку синтаксиса для чего-либо и не знаю, делает ли кто-нибудь это на самом деле.)

Я полностью согласен с этим. await - это операция потока управления, например break или return , и она должна быть явной.

Мы согласны. Обозначения foo.await , foo await , foo# , ... являются явными . Нет никакого неявного ожидания.

Я также не понимаю, почему e.await более согласуется с синтаксисом Rust, чем await!(e) или await e .

Синтаксис e.await по себе не согласуется с синтаксисом Rust, но постфикс обычно лучше подходит для ? и того, как структурированы API Rust (методы предпочтительнее бесплатных функций).

Синтаксис await e? , если он связан как (await e)? , полностью несовместим с тем, как связываются break и return . await!(e) также несовместим, поскольку у нас нет макросов для потока управления, и он также имеет ту же проблему, что и другие методы префикса.

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

Я не думаю, что вам вообще нужно менять libsyntax на .await так как это уже должно обрабатываться как полевая операция. Логику лучше рассматривать в разрешении или HIR, где вы переводите ее в специальную конструкцию.

Мы можем выбрать необычный и замечательный синтаксис, если у него есть замечательные преимущества. Но есть ли они в постфиксе await ?

Как уже упоминалось, я утверждаю, что это происходит из-за цепочки методов и предпочтения Rust для вызовов методов.

Я не думаю, что вам вообще нужно менять libsyntax для .await, так как это уже должно обрабатываться как полевая операция.

Это весело.
Таким образом, идея состоит в том, чтобы повторно использовать подход self / super ..., но для полей, а не для сегментов пути.

Это фактически делает await ключевым словом сегмента пути (поскольку оно проходит через разрешение), поэтому вы можете запретить для него

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}

Нет никакого неявного ожидания.

Идея возникала несколько раз в этой ветке (предложение «неявное ожидание»).

у нас нет макросов для потока управления

Есть try! (который неплохо справился со своей задачей) и, возможно, устаревший select! . Обратите внимание, что await «сильнее», чем return , поэтому вполне разумно ожидать, что он будет более заметным в коде, чем ? return .

Я утверждаю, что это происходит из-за цепочки методов и предпочтения Rust для вызовов методов.

Он также имеет (более заметное) предпочтение для операторов потока управления префиксом.

В ожидании е? синтаксис, если он связан как (await e)? совершенно несовместимо с тем, как связать break и return.

Я предпочитаю await!(e)? , await { e }? или, может быть, даже { await e }? - я не думаю, что видел последнее обсуждение, и я не уверен, работает ли это.


Я допускаю, что у меня может быть предубеждение слева направо. _Примечание_

Мое мнение по этому поводу, кажется, меняется каждый раз, когда я смотрю на проблему, как будто играю для себя адвоката Дьявола. Отчасти это связано с тем, что я так привык писать свои собственные фьючерсы и конечные автоматы. Пользовательское будущее с poll совершенно нормально.

Возможно, об этом стоит подумать по-другому.

Для меня абстракции с нулевой стоимостью в Rust означают две вещи: нулевую стоимость во время выполнения и, что более важно, нулевую стоимость мысленно.

Я могу очень легко рассуждать о большинстве абстракций в Rust, включая фьючерсы, потому что они всего лишь конечные автоматы.

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

Возможно, лучшее решение - самое простое - оригинальный макрос await! .

Итак, при всем уважении к этим другим языкам, внутренняя согласованность в Rust кажется более важной, и я не думаю, что синтаксис префиксов подходит Rust ни с точки зрения приоритета, ни с точки зрения структуры API.

Я не понимаю, как ...? await(foo)? / await { foo }? кажется совершенно нормальным с точки зрения приоритета операторов и того, как API структурированы в Rust - его обратная сторона - многословие скобок и (в зависимости от вашей точки зрения) цепочка, не нарушающая прецедента или сбивает с толку.

Есть try! (который неплохо справился со своей задачей) и, возможно, устаревший select! .

Я думаю, что ключевое слово здесь устарело . Использование try!(...) является серьезной ошибкой в Rust 2018. Сейчас это серьезная ошибка, потому что мы представили лучший, первоклассный синтаксис с постфиксами.

Обратите внимание, что await «сильнее», чем return , поэтому вполне разумно ожидать, что он будет более заметным в коде, чем ? return .

Оператор ? также может иметь побочные эффекты (с помощью других реализаций, кроме Result ) и выполняет поток управления, поэтому он также довольно "сильный". Когда это обсуждалось, ? обвиняли в «сокрытии возврата» и в том, что его легко не заметить. Я считаю, что это предсказание полностью не сбылось. Ситуация повторная. await мне кажется очень похожим.

Он также имеет (более заметное) предпочтение для операторов потока управления префиксом.

Эти операторы потока управления префиксом набираются как ! type. Между тем, другой оператор потока управления ? который принимает контекст impl Try<Ok = T, ...> и дает вам T является постфиксом.

Я не понимаю, как ...? await(foo)? / await { foo }? кажется совершенно нормальным с точки зрения приоритета операторов и того, как API структурированы в Rust-

Синтаксис await(foo) не совпадает с синтаксисом await foo если круглые скобки требуются для первого, а не для второго. Первое беспрецедентно, второе имеет проблемы с приоритетом. ? как мы обсуждали здесь, в блоге лодки и на Discord. Синтаксис await { foo } проблематичен по другим причинам (см. Https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611).

его обратная сторона - многословие парных знаков и (в зависимости от вашей точки зрения) цепочка, не нарушающая прецедент и не сбивающая с толку.

Вот что я имею в виду под «структурированными API». Я думаю, что методы и их объединение в Rust обычны и идиоматичны. Синтаксисы префикса и блока плохо сочетаются с ними и с ? .

Я могу быть в меньшинстве с таким мнением, и если это так, игнорируйте меня:

Было бы справедливо переместить обсуждение префиксов и постфиксов в ветку Внутренних дел, а затем просто вернуться сюда с результатом? Таким образом, мы сможем сохранить проблему отслеживания для

@seanmonstar Да, я очень сочувствую желанию ограничить обсуждение проблем с отслеживанием и иметь проблемы, которые на самом деле являются просто обновлениями статуса. Это одна из проблем, которые, я надеюсь, мы сможем решить с помощью некоторых изменений в том, как мы управляем процессом RFC и проблемами в целом. На данный момент, я открыл новый вопрос здесь для нас использовать для обсуждения.

ВАЖНО ДЛЯ ВСЕХ: дальнейшее обсуждение синтаксиса await должно идти здесь .

Временная блокировка на день, чтобы обеспечить дальнейшее обсуждение синтаксиса await по соответствующей проблеме.

Во вторник, 15 января 2019 г., в 07:10:32 -0800 Пауан написал:

Примечание: всегда можно использовать скобки, чтобы прояснить приоритет:

let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;

Это сводит на нет основное преимущество postfix await: "просто продолжайте
запись / чтение ". Postfix await, как postfix ? , разрешает поток управления
продолжать движение слева направо:

foo().await!()?.bar().await!()

Если await! был префиксом, или назад, когда try! был префиксом, или если у вас
заключить в круглые скобки, тогда вам нужно вернуться к левой части
выражение при его написании или чтении.

РЕДАКТИРОВАТЬ: Я читал комментарии от начала до конца по электронной почте и не видел комментариев «переместить разговор в другую проблему» до тех пор, пока не отправил это письмо.

Отчет о состоянии async-await:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


Я хотел опубликовать быстрое обновление статуса async-await
усилие. Если коротко, то мы на финишной прямой.
какая-то стабилизация, но остаются некоторые существенные
вопросы, которые нужно преодолеть.

Объявление рабочей группы по внедрению

В рамках этой кампании я рад сообщить, что мы сформировали
рабочая группа по реализации async-await . Эта рабочая группа
является частью всего усилия async-await, но сосредоточено на
реализации и является частью команды компилятора. Если ты хочешь
помогите пройти async-await до финиша, у нас есть список проблем
где нам определенно нужна помощь (читайте дальше).

Если вы заинтересованы в участии, у нас есть «часы работы».запланировано на вторник (см. [календарь группы составителей]) - если вы
может появиться тогда на [Zulip], это было бы идеально! (Но если нет, просто вставьте любой
время.)

...

Когда std::future::Future будет в стабильной версии? Должен ли он ждать async await? Я считаю, что это очень хороший дизайн, и хотел бы начать переносить на него код. (Есть ли прокладка для использования в стойле?)

@ry см. свежую проблему отслеживания для него: https://github.com/rust-lang/rust/issues/59113

Еще одна проблема компилятора для async / await: https://github.com/rust-lang/rust/issues/59245

Также обратите внимание, что https://github.com/rust-lang-nursery/futures-rs/issues/1199 в верхнем посте можно пометить, так как теперь это исправлено.

Похоже, есть проблема с HRLB и асинхронным закрытием: https://github.com/rust-lang/rust/issues/59337. (Хотя при повторном просмотре RFC он фактически не указывает, что асинхронные замыкания подлежат тому же захвату времени жизни аргумента, что и асинхронная функция).

Да, асинхронные закрытия имеют множество проблем, и их не следует включать в начальный этап стабилизации. Текущее поведение можно эмулировать с помощью блока closure + async, и в будущем я бы хотел увидеть версию, которая позволяла бы ссылаться на upvars закрытия из возвращенного future-объекта.

Я только что заметил, что в настоящее время await!(fut) требует, чтобы fut был Unpin : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist= 9c189fae3cfeecbb041f68f02f31893d

Это ожидается? Похоже, этого нет в RFC.

@Ekleog, который не await! дает ошибку, await! концептуально закрепляет прошедшее будущее, чтобы можно было использовать фьючерсы !Unpin ( быстрый пример игровой площадки ). Ошибка возникает из-за ограничения на impl Future for Box<impl Future + Unpin> , которое требует, чтобы будущее было Unpin чтобы вы не делали что-то вроде:

// where Foo: Future + !Unpin
let mut foo: Box<Foo> = ...;
Pin::new(&mut foo).poll(cx);
let mut foo = Box::new(*foo);
Pin::new(&mut foo).poll(cx);

Поскольку Box - это Unpin и позволяет перемещать значение из него, вы можете опросить будущее один раз в одном месте кучи, а затем переместить будущее из коробки и поместить его в новое место кучи и опрос это снова.

await, возможно, должен быть в специальном регистре, чтобы разрешить Box<dyn Future> поскольку он потребляет будущее

Может быть, черту IntoFuture нужно воскресить за await! ? Box<dyn Future> может реализовать это путем преобразования в Pin<Box<dyn Future>> .

А вот и моя следующая ошибка с async / await: похоже, что использование связанного типа с параметром типа в возвращаемом типе async fn нарушает вывод: https://github.com/rust-lang/rust/ issues / 60414

В дополнение к потенциальному добавлению # 60414 в список верхнего поста (не знаю, используется ли он до сих пор - может быть, лучше указать на метку github?), Я думаю, что «Разрешение rust-lang / rfcs # 2418 », так как свойство IIRC Future недавно стабилизировалось.

Я только что пришел из сообщения Reddit и должен сказать, что мне вообще не нравится синтаксис постфикса. И похоже, что большинству Reddit это тоже не нравится.

Я лучше напишу

let x = (await future)?

чем принять этот странный синтаксис.

Что касается цепочки, я могу провести рефакторинг своего кода, чтобы не было больше 1 await .

Кроме того, JavaScript в будущем сможет это сделать ( предложение умного конвейера ):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

Если реализован префикс await , это не означает, что await нельзя связать.

@KSXGitHub, это действительно не место для этого обсуждения, но здесь изложена логика, и для этого есть очень веские причины, которые многие люди придумывали в течение многих месяцев

@KSXGitHub Хотя мне также не нравится окончательный синтаксис, он подробно обсуждался в # 57640, https://internals.rust-lang.org/t/await-syntax-discussion-summary/ , https://internals.rust- lang.org/t/a-final-proposal-for-await-syntax/ и в других местах. Многие люди высказали там свои предпочтения, и вы не приводите никаких новых аргументов по этому поводу.

Пожалуйста, не обсуждайте здесь дизайнерские решения, для этой явной цели есть ветка

Если вы планируете комментировать там, пожалуйста, имейте в виду, что обсуждение уже довольно сильно разыгралось: убедитесь, что у вас есть что-то существенное, чтобы сказать, и убедитесь, что этого не было сказано ранее в ветке.

@withoutboats, насколько я понимаю, окончательный синтаксис уже согласован, может быть, пора пометить его как выполненное? :краснеть:

Есть ли намерение стабилизироваться ко времени следующей бета-версии, которая состоится 4 июля, или для устранения блокирующих ошибок потребуется еще один цикл? В теге A-async-await есть множество открытых проблем, хотя я не уверен, сколько из них является критическим.

Привет! Когда нам ждать стабильного выпуска этой функции? И как я могу использовать это в ночных сборках?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149 содержит информацию о целевой дате выпуска и многое другое.

Планируется ли автоматическая реализация Unpin для фьючерсов, генерируемых async fn ?

В частности, мне интересно, недоступен ли Unpin автоматически из-за сгенерированного самого кода Future, или потому что мы можем использовать ссылки в качестве аргументов

@DoumanAsh Я полагаю, что если async fn никогда не имеет активных ссылок на себя в точках выхода, то сгенерированное Future, возможно, может реализовать Unpin, возможно?

Я думаю, это должно сопровождаться некоторыми довольно полезными сообщениями об ошибках, в которых говорится «не Unpin из-за _this_ заимствования» + намек на «в качестве альтернативы вы можете упаковать это будущее»

В PR стабилизации №63209 указано, что «Все блокираторы закрыты». и был запущен в ночное время 20 августа, поэтому готовится к бета-версии позднее на этой неделе. Похоже, стоит отметить, что с 20 августа было зарегистрировано несколько новых проблем с блокировкой (что отслеживается тегом AsyncAwait-Blocking). Две из них (# 63710, # 64130) кажутся полезными, которые на самом деле не будут препятствовать стабилизации, однако есть три других проблемы (# 64391, # 64433, # 64477), которые, кажется, заслуживают обсуждения. Эти последние три проблемы связаны между собой, и все они возникают из-за PR № 64292, который сам был загружен для решения проблемы AsyncAwait-Blocking № 63832. PR, # 64584, уже приземлился в попытке решить большую часть проблем, но три вопроса остаются открытыми на данный момент.

Положительным моментом является то, что три серьезных открытых блокиратора, похоже, касаются кода, который должен компилироваться, но в настоящее время не компилируется. В этом смысле он будет обратно совместим с исправлениями позже, не препятствуя бета -изации и возможной стабилизации async / await. Тем не менее, мне интересно, думает ли кто-нибудь из команды lang, что здесь есть что-то достаточно важное, чтобы предложить, чтобы async / await запускались каждую ночь для другого цикла (что, как бы неприятно это ни звучало, является точкой графика быстрого выпуска после всего).

@bstrie Мы просто повторно используем «AsyncAwait-Blocking» из-за отсутствия лучшего ярлыка, чтобы отметить их как «высокоприоритетные», на самом деле они не блокируют. Вскоре нам следует обновить систему маркировки, чтобы она не сбивала с толку, cc @nikomatsakis.

... Плохо ... мы пропустили async-await в ожидаемой версии 1.38. Приходится ждать 1.39 только потому, что некоторые "проблемы" не в счет ...

@earthengine Я не думаю, что это справедливая оценка ситуации. Все возникшие проблемы заслуживают серьезного отношения. Было бы нехорошо использовать async await только для того, чтобы люди затем столкнулись с этими проблемами, пытаясь использовать его на практике :)

Была ли эта страница полезной?
0 / 5 - 0 рейтинги