Rust: async/await 的跟踪问题 (RFC 2394)

创建于 2018-05-08  ·  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 关键字的语法。 如果某些东西是 Result 的未来——就像任何 IO 未来可能是——你希望能够等待它,然后将?运算符应用于它。 但是启用此功能的优先顺序可能看起来令人惊讶 - await io_future? await首先是? ,尽管?在词汇上比 await 更紧密地绑定。

我同意这里,但大括号是邪恶的。 我认为更容易记住?优先级低于await并以它结尾:

let foo = await future?

它更易于阅读,更易于重构。 我相信这是更好的方法。

let foo = await!(future)?

允许更好地理解执行操作的顺序,但它的可读性较差。

我确实相信,一旦你得到await foo?执行await那么你就没有问题了。 它可能在词汇上更紧密,但await位于左侧,而?位于右侧。 所以它仍然足够合乎逻辑,首先await并在它之后处理Result


如果存在任何分歧,请表达出来,以便我们讨论。 我不明白无声的downvote 代表什么。 我们都希望 Rust 好。

所有308条评论

这里的讨论似乎已经平息,因此将其链接为await语法问题的一部分: https :

实施在#50307 上被阻止。

关于语法:我真的很想将await作为简单的关键字。 例如,让我们看一下博客中的一个问题:

我们并不完全确定我们想要的 await 关键字的语法。 如果某些东西是 Result 的未来——就像任何 IO 未来可能是——你希望能够等待它,然后将?运算符应用于它。 但是启用此功能的优先顺序可能看起来令人惊讶 - await io_future? await首先是? ,尽管?在词汇上比 await 更紧密地绑定。

我同意这里,但大括号是邪恶的。 我认为更容易记住?优先级低于await并以它结尾:

let foo = await future?

它更易于阅读,更易于重构。 我相信这是更好的方法。

let foo = await!(future)?

允许更好地理解执行操作的顺序,但它的可读性较差。

我确实相信,一旦你得到await foo?执行await那么你就没有问题了。 它可能在词汇上更紧密,但await位于左侧,而?位于右侧。 所以它仍然足够合乎逻辑,首先await并在它之后处理Result


如果存在任何分歧,请表达出来,以便我们讨论。 我不明白无声的downvote 代表什么。 我们都希望 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@? (这是“线路噪音”)。
  • 完全不使用语法,使 await 隐式(这使得暂停点更难看到)。 为此,还必须明确构建未来的行为。 这是我上面链接的内部线程的主题。

这就是说,确实有asyncawait甚至任何意义,功能是否明智?

@alexreg确实如此。 例如,Kotlin 就是这样工作的。 这是“隐式等待”选项。

@rpjohnst有趣。 好吧,我通常将asyncawait作为该语言的显式特征,因为我认为这更符合 Rust 的精神,但我不是异步编程方面的专家。 ..

@alexreg async/await 是非常好的功能,因为我每天都在使用 C#(这是我的主要语言)使用它。 @rpjohnst 很好地分类了所有可能性。 我更喜欢第二种选择,我同意其他方面的考虑(嘈杂/不寻常/...)。 在过去的 5 年里,我一直在使用 async/await 代码,拥有这样一个标志关键字非常重要。

@rpjohnst

所以即使我们只是选择了一个优先级,我们仍然会遇到 await (await (await (await first()?).second()?).third()? 的问题。

在我的实践中,你永远不会在一行中写两个await 。 在极少数情况下,当您需要它时,您只需将其重写为then并且根本不使用 await。 你可以看到自己比阅读更难

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

所以我认为如果语言不鼓励以这种方式编写代码以使主要情况更简单和更好,那也没关系。

hero away future await?虽然不熟悉但看起来很有趣,但我没有看到任何逻辑上的反驳。

在我的实践中,你永远不会在一行中写两个await

但这是因为无论语法如何,这都是一个坏主意,还是仅仅因为 C# 的现有await语法使它变得丑陋? 人们围绕try!()?的前身)提出了类似的论点。

后缀和隐式版本远没有那么难看:

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

但这是因为无论语法如何,这都是一个坏主意,还是仅仅因为 C# 的现有 await 语法使它变得丑陋?

我认为无论语法如何,这都是一个坏主意,因为每个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只会使 oneliner 更难以编写和阅读,但我确实这样做了相信这不是它不好的唯一原因。

后缀和隐式版本远没有那么难看

区分任务开始和任务等待的可能性非常重要。 例如,我经常写这样的代码(同样是项目的一个片段):

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 个异步请求,然后等待它们。 我们不会在每次循环迭代中等待,但首先我们创建异步请求数组,然后立即等待它们。

我不知道 Kotlin,所以也许他们以某种方式解决了这个问题。 但是,如果“运行”和“等待”任务是相同的,我不知道如何表达它。


所以我认为隐式版本在像 C# 这样的隐式语言中是不可能的。
在 Rust 中,它的规则甚至不允许您将u8隐式转换i32这会更加混乱。

@Pzixel是的,第二个选项听起来像是更可取的选项之一。 我也在 C# 中使用过async/await ,但不是很多,因为我已经有几年没有主要用 C# 编程了。 至于优先级, await (future?)对我来说更自然。

@rpjohnst我有点喜欢后缀运算符的想法,但我也担心人们会做出的可读性和假设——它很容易被名为awaitstruct的成员弄糊涂.

区分任务开始和任务等待的可能性非常重要。

对于它的价值,隐式版本确实做到了这一点。 它无论是在RFC线程,并在内部讨论线程死亡,所以我不会去到很多细节在这里,但其基本思想是只知道它移动显性从任务等待对该任务建材它doesn '不引入任何新的隐性。

您的示例如下所示:

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 /etc。

对于它的价值,隐式版本确实做到了这一点。 它在 RFC 线程和内部线程中都被讨论到死,所以我不会在这里详细介绍,但基本思想只是它将明确性从任务等待转移到任务构建 - 它没有'不引入任何新的隐性。

我相信确实如此。 我在这里看不到这个函数中的流程是什么,在等待完成之前执行中断的点在哪里。 我只看到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上中断。 它们彼此非常相似。 所以它要么不像原始代码那样工作,要么太复杂,即使我非常熟悉这个主题,我也看不到它。 这两种选择都不会使这个选项成为一种选择。

我在这里看不到这个函数中的流程是什么,在等待完成之前执行中断的点在哪里。

是的,这就是我所说的“这使得悬挂点更难看到”的意思。 如果您阅读了链接的内部线程,我会论证为什么这不是那么大的问题。 您不必编写任何新代码,只需将注释放在不同的位置( async块而不是await ed 表达式)。 IDE 可以毫无问题地说明类型是什么(对于函数调用总是T ,对于async块总是T Future<Output=T> )。

我还要指出,无论语法如何,您的理解都可能是错误的。 Rust 的async函数在以某种方式等待它们之前根本不会运行任何代码,因此您的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))

没有更多的上下文。

嵌套块会变得更奇怪。 更不用说关于工具等的问题了。

呼叫站点行为变得依赖于上下文

对于普通的同步代码和闭包来说,这已经是正确的。 例如:

// 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上下文中调用async fn s。 这意味着函数调用语法some_function(arg1, arg2, etc)总是在调用者继续之前运行some_function的主体,无论some_function是否为async 。 所以进入async上下文总是被显式标记,并且函数调用语法实际上更加一致。

关于 await 语法:带有方法语法的宏怎么样? 我找不到允许这样做的实际 RFC,但我在 reddit 上发现了一些讨论( 12 ),因此这个想法并非史无前例。 这将允许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 世界中有一个库可以简化 monad 组合: http :

也许一些想法对 Rust 很有趣。

引用自文档:

大多数主流语言都支持使用 async/await 惯用语的异步编程或正在实现它(例如 F#、C#/VB、Javascript、Python、Swift)。 尽管很有用,但 async/await 通常与表示异步计算(Task、Future 等)的特定 monad 相关联。

这个库实现了一个类似于 async/await 的解决方案,但可以推广到任何 monad 类型。 考虑到一些代码库除了 Future 之外还使用 Task 等其他 monad 进行异步计算,这种概括是一个主要因素。

给定一个 monad M ,泛化使用将常规值提升到 monad ( T => M[T] ) 和从 monad 实例 ( M[T] => T ) 中解除值的概念。 > 示例用法:

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

请注意,lift 对应于 async 和 unlift to await。

对于普通的同步代码和闭包来说,这已经是正确的。 例如:

我在这里看到了几个不同之处:

  1. Lambda 上下文是不可避免的,但它不适用于await 。 对于await我们没有上下文,对于async我们必须有一个。 前者胜出,因为它提供了相同的功能,但对代码的了解较少。
  2. Lambdas 往往很短,最多几行,所以我们一次看到整个身体,而且很简单。 async函数可能很大(和常规函数一样大)和复杂。
  3. Lambda 很少嵌套(除了then调用,但建议await用于), async块经常嵌套。

从完整的隐式等待提案中您应该注意的另一件事是您不能从非异步上下文调用 async fns。

嗯,我没注意到。 这听起来不太好,因为在我的实践中,您经常希望从非异步上下文运行异步。 在 C# 中async只是一个允许编译器重写函数体的关键字,它不会以任何方式影响函数接口,所以async Task<Foo>Task<Foo>是完全可以互换的,并且将实现和 API 解耦。

有时您可能想阻止async任务,例如,当您想从main调用某些网络 API 时。 您必须阻止(否则您将返回操作系统并结束程序)但您必须运行异步 HTTP 请求。 我不知道这里有什么解决方案,除了 hacking main以允许它像我们使用Result main 返回类型一样异步,如果你不能从非异步 main 调用它.

支持当前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 async fn s 更加一致,因为调用声明为-> T总是会给你一个T ,无论它是否async

(你也应该注意到,在锈仍有之间相当飞跃async fnFuture -returning版本,如RFC描述的async fn版本并没有提及Future在其签名中的任何位置;手动版本需要impl Trait ,这会带来一些与生命周期有关的问题。事实上,这是async fn部分动机

它可以更轻松地从其他语言(例如 C#/TypeScript/JS/Python)迁移

仅对文字await future语法有利,这在 Rust 中本身就存在相当大的问题。 我们最终可能得到的任何其他东西也与这些语言不匹配,而隐式 await 至少具有 a) 与 Kotlin 的相似性和 b) 与同步的、基于线程的代码的相似性。

是的,这是提案之间的合理差异。 它也包含在内部线程中。 可以说,为两者设置不同的接口很有用

我会说_为两者使用不同的接口有一些优势_,因为让 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
}

它变成了一个突破性的变化。 哇?

这仅对于字面量的 await future 语法是一个优势,这在 Rust 中本身就存在相当大的问题。 我们最终可能得到的任何其他东西也与这些语言不匹配,而隐式 await 至少具有 a) 与 Kotlin 的相似性和 b) 与同步的、基于线程的代码的相似性。

对于任何await语法、 await foo / foo await / foo@ / foo.await /...同样的事情,唯一的区别是你把它放在之前/之后或者有一个印记而不是关键字。

您还应该注意到,在 Rust 中,async fn 和 Future-returning 版本之间仍有很大的飞跃,如 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
    }
}

在生态系统从手动未来实现和组合器(今天的唯一方法)过渡到异步/等待期间,出现这种区别的更大问题。 但即便如此,该提案仍允许您保留旧界面并在其旁边提供一个新的异步界面。 例如,C# 充满了这种模式。

嗯,这听起来很有道理。

但是,我确实相信这种隐含性(我们不知道这里的foo()是异步函数还是同步函数)会导致 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)? vs. await foo? ,我是前阵营的。 您几乎可以内化任何语法,但是我们太习惯于从间距和接近度中获取线索。 使用await foo? ,人们可能会在两个运算符的优先级上进行第二次猜测,而大括号则清楚地表明发生了什么。 保存三个字符是不值得的。 至于链接await!的做法,虽然它在某些语言中可能是一个流行的习惯用法,但我觉得它有太多的缺点,例如可读性差和与调试器的交互不值得优化。

保存三个字符是不值得的。

根据我的轶事经验,额外的字符(例如更长的名称)并不是什么大问题,但额外的标记可能真的很烦人。 就 CPU 类比而言,长名称是具有良好局部性的直线代码 - 我可以从肌肉记忆中输入它 - 而当涉及多个令牌(例如标点符号)时相同数量的字符是分支且充满缓存未命中。

(我完全同意await foo?非常不明显,我们应该避免它,并且必须键入更多标记会更好;我的观察只是并非所有字符都生而平等。)


@rpjohnst我认为如果您的替代提案以“显式异步”而不是“隐式等待”形式呈现,它的接受度可能会稍好一些:-)

了解bar是同步函数还是异步函数至关重要。

我不确定这与知道某个函数是便宜还是昂贵,或者它是否执行 IO,或者它是否触及某些全局状态真的有什么不同。 (这也适用于@lnicola的层次结构——如果异步调用像同步调用一样运行完成,那么它们在功率方面真的没有什么不同!)

例如,调用处于循环中这一事实与它是异步的这一事实一样重要,如果不是更重要的话。 在 Rust 中,并行化更容易实现,您也可以建议将看起来昂贵的同步循环切换到 Rayon 迭代器!

所以我不认为需要await实际上对于捕捉这些优化来说那么重要。 循环一直是寻找优化的好地方,而async fn已经是一个很好的指标,表明您可以获得一些廉价的 IO 并发。 如果您发现自己错过了这些机会,您甚至可以为偶尔运行的“循环中的异步调用”编写一个 Clippy lint。 如果同步代码也有类似的 lint,那就太好了!

正如@lnicola暗示的那样,“显式异步”的动机不仅仅是“更少的语法”。 这是为了使函数调用语法的行为更加一致,以便foo()始终运行foo的主体直到完成。 在这个提议下,省略一个注解只会给你更少并发的代码,这就是几乎所有代码的行为方式。 在“显式等待”下,省略注释会导致意外并发,或者至少是意外交错,这是有问题的

我认为如果您的替代提案以“显式异步”而不是“隐式等待”形式呈现,它的接受度可能会稍好一些:-)

该线程名为“显式未来构造,隐式等待”,但似乎后一个名称已被卡住。 :P

我不确定这与知道某个函数是便宜还是昂贵,或者它是否执行 IO,或者它是否触及某些全局状态真的有什么不同。 (这也适用于@lnicola的层次结构——如果异步调用像同步调用一样运行完成,那么它们在功率方面真的没有什么不同!)

我认为这与知道函数会改变某些状态一样重要,而且我们在调用方和调用方都有一个mut关键字。

正如@lnicola暗示的那样,“显式异步”的动机不仅仅是“更少的语法”。 是为了使函数调用语法的行为更加一致,以便 foo() 始终运行 foo 的主体直到完成。

一方面,这是一个很好的考虑。 另一方面,您可以轻松地将未来的创建和未来的运行分开。 我的意思是,如果foo返回一些抽象,允许你调用run并得到一些结果,它不会使foo无用的垃圾,它不会做任何事情,它做了一个非常有用的东西:它构造了一些你可以稍后调用方法的对象。 它不会让它有任何不同。 我们调用的foo方法只是一个黑盒,我们看到它的签名Future<Output=T>并且它实际上返回了一个未来。 所以当我们想要这样做时,我们明确地await它。

该线程名为“显式未来构造,隐式等待”,但似乎后一个名称已被卡住。 :P

我个人认为更好的选择是“显式异步显式等待”:)


聚苯乙烯

今晚我也被一个想法击中了:你是否尝试过与 C# LDM 通信? 例如,像@HaloFour@gafter@CyrusNajmabadi 这样的人。 问他们为什么采用他们采用的语法可能是一个很好的主意。 我也建议问问其他语言的人,但我只是不认识他们 :) 我确信他们对现有语法有过多次辩论,他们已经可以讨论很多了,他们可能有一些有用的想法。

这并不意味着 Rust 必须有这种语法,因为 C# 有,但它只是允许做出更重要的决定。

我个人认为更好的选择是“显式异步显式等待”:)

不过,主要的提议不是“显式异步”——这就是我选择这个名字的原因。 它是“隐式异步”,因为您无法一眼看出异步是从哪里引入的。 任何未注释的函数调用都可能在不等待的情况下构造未来,即使Future在其签名中没有出现。

就其价值而言,内部线程确实包含一个“显式异步显式等待”替代方案,因为这与任一主要替代方案未来兼容。 (请参阅第一篇文章的最后一部分。)

您是否尝试与 C# LDM 进行通信?

主要 RFC 的作者这样做了。 说出来的话,据我记住的要点,是决定不包括Future中的签名async fn秒。 在 C# 中,您可以用其他类型替换Task来控制函数的驱动方式。 但是在 Rust 中,我们没有(也不会)有任何这样的机制——所有的期货都会经历一个单一的特征,所以没有必要每次都写出那个特征。

我们还与 Dart 语言设计者进行了交流,这是我编写“显式异步”提案的很大一部分动机。 Dart 1 有一个问题,因为函数在调用时没有运行到它们的第一个等待(与 Rust 的工作方式不完全相同,但类似),这导致了如此大的混乱,以至于在 Dart 2 中它们发生了变化,因此函数确实运行到了它们的第一个调用时等待。 由于其他原因,Rust 不能这样做,但它可以在调用时运行整个函数,这也可以避免这种混淆。

我们还与 Dart 语言设计者进行了交流,这是我编写“显式异步”提案的很大一部分动机。 Dart 1 有一个问题,因为函数在调用时没有运行到它们的第一个等待(与 Rust 的工作方式不完全相同,但类似),这导致了如此大的混乱,以至于在 Dart 2 中它们发生了变化,因此函数确实运行到了它们的第一个调用时等待。 由于其他原因,Rust 不能这样做,但它可以在调用时运行整个函数,这也可以避免这种混淆。

很棒的经历,我不知道。 很高兴听到您完成了如此庞大的工作。 干得好👍

今晚我也被一个想法击中了:你是否尝试过与 C# LDM 通信? 例如,像@HaloFour@gafter@CyrusNajmabadi 这样的人。 问他们为什么采用他们采用的语法可能是一个很好的主意。

我很乐意提供您感兴趣的任何信息。但是,我只是略读了一下。 是否可以浓缩您目前遇到的任何具体问题?

关于await语法(这可能是完全愚蠢的,随时对我大喊大叫;我是一个异步编程菜鸟,我不知道我在说什么):

除了使用“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 f?评估?运算符,然后等待结果未来。
  • (await f)?首先等待未来,然后根据结果计算?运算符(由于普通的 Rust 运算符优先级)
  • await? f可用作`(await f)? 的语法糖。 我相信“未来返回结果”将是一个非常常见的情况,因此专用语法很有意义。

我同意其他评论者的意见,即await应该是明确的。 在 JavaScript 中这样做非常轻松,我真的很欣赏 Rust 代码的明确性和可读性,我觉得让 async 隐式会破坏异步代码的这一点。

我突然想到“隐式异步块”应该可以作为 proc_macro 来实现,它只是在任何未来之前插入一个await关键字。

主要问题是您认为列出的选项中哪个选项更适合当前的 Rust 语言,

询问 C# 设计师什么最适合 Rust 语言是......很有趣:)

我觉得没有资格做出这样的决定。 我喜欢生锈并涉足它。 但这不是我日复一日使用的语言。 我也没有把它深深地印在我的心里。 As such, i don't think i'm qualified to to make any claims about what are the appropriate choices for this language here. 想请教一下 Go/TypeScript/C#/VB/C++。 当然,我会觉得舒服多了。 但是 Rust 超出了我的专业领域,无法对任何此类想法感到满意。

反对 C# 语法的主要考虑因素是运算符优先级await foo?

这是我觉得我可以评论的事情。 我们对“await”考虑了很多优先级,在设置我们想要的表格之前,我们尝试了很多表格。 我们发现的核心问题之一是,对于我们以及想要使用此功能的客户(内部和外部)而言,人们很少真正想将任何东西“链接”到他们的异步调用之后。 换句话说,人们似乎强烈倾向于“等待”是任何完整表达中最重要的部分,因此它靠近顶部。 注意:“完整表达式”是指您在表达式语句顶部得到的表达式,或顶级赋值右侧的 hte 表达式,或者作为“参数”传递给某物的表达式。

人们想要在 expr 中使用“await”来“继续”的倾向是罕见的。 我们偶尔会看到诸如(await expr).M() ,但与执行await expr.M()的人数相比,这些似乎不太常见,也不太理想。

这也是我们没有为“await”使用任何“隐式”形式的原因。 在实践中,这是人们想要非常清楚地思考的事情,并且他们希望在他们的代码中处于前端和中心位置,以便他们能够关注它。 有趣的是,即使多年后,这种趋势仍然存在。 即,有时我们会在多年后为某些内容过于冗长而感到遗憾。 一些功能在早期就以这种方式很好,但是一旦人们对它感到满意,就更适合更简洁的东西。 'await' 的情况并非如此。 人们似乎仍然很喜欢该关键字的重量级性质和我们选择的优先级。

到目前为止,我们观众的优先选择感到非常满意。 将来,我们可能会在这里进行一些更改。 但总体而言,这样做的压力并不大。

——

以及与 C# 执行不同,直到第一次等待才在调用者线程中运行,但根本不启动的区别,当前代码片段的相同方式在第一次调用 GetEnumerator 之前不会运行否定性检查:

IMO,我们做枚举器的方式有点错误,多年来导致了一堆混乱。 它特别糟糕,因为很多代码必须像这样编写:

```c#
void SomeEnumerator(X args)
{
// 验证 Args,做同步工作。
返回 SomeEnumeratorImpl(args);
}

void SomeEnumeratorImpl(X args)
{
// ...
屈服
// ...
}

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)? 的语法糖。 我相信“未来返回结果”将是一个非常常见的情况,因此专用语法很有意义。

这是最能引起我共鸣的。 它允许 'await' 成为最重要的概念,但也允许简单处理 Result 类型。

我们从 C# 中了解到的一件事是,人们对优先级的直觉与空格有关。 所以如果你有“await x?” 然后立即感觉await优先级低于?因为?紧靠表达式。 如果上面的内容实际上解析为(await x)? ,那会让我们的观众感到惊讶。

将它解析为await (x?)会感觉最自然的语法,并且将满足获取未来/任务的“结果”的需要,并希望“等待”,如果你真的收到了一个值. 如果然后返回一个 Result 本身,那么将它与“await”结合起来以表明它随后发生是合适的。 所以await? x?每个?与它最自然相关的代码部分紧密绑定。 第一个?await (特别是它的结果)相关,第二个与x

如果“执行不会在调用者线程中运行直到第一次等待”这里实际发生了什么?

什么也没有发生,直到调用者等待的返回值FooAsync ,此时FooAsync的身体奔跑,直到某个await或返回。

它是这样工作的,因为 Rust Future是轮询驱动的,堆栈分配的,并且在第一次调用poll后不可移动。 调用者必须有机会将它们移动到位 - 在堆上为顶级Future s,或者在父Future按值移动,通常在“堆栈框架”上调用async fn在执行任何代码之前。

这意味着我们被困在 a) 类似 C# 生成器的语义上,其中没有代码在调用时运行,或者 b) 类似 Kotlin 协程的语义,其中调用函数立即隐式地等待它(类似闭包的async { .. }当您确实需要并发执行时,

我比较喜欢后者,因为它避免了您在 C# 生成器中提到的问题,也完全避免了运算符优先级问题。

@CyrusNajmabadi在 Rust 中, Future通常在它作为Task生成之前不起作用(它更类似于 F# Async ):

let bar = foo();

在这种情况下, foo()返回Future ,但它实际上可能没有任何事情。 您必须手动生成它(这也类似于 F# Async ):

tokio::run(bar);

当它生成时,它将运行Future 。 由于这是Future的默认行为,因此 Rust 中的 async/await 在生成之前运行任何代码会更加一致。

显然情况在 C# 中有所不同,因为在 C# 中,当您调用foo()它会立即开始运行Task ,因此在 C# 中运行代码直到第一个await才有意义.

另外......如果你有这样的事情,那么事情如何工作[...]你可以完全调用这个方法并且永远不会等待。 如果“执行不会在调用者线程中运行直到第一次等待”这里实际发生了什么?

如果您调用FooAsync()则它什么都不做,也不会运行任何代码。 然后当你生成它时,它会同步运行代码, await永远不会运行,所以它立即返回() (这是void Rust 版本)

换句话说,它不是“执行不会在调用者线程中运行,直到第一次等待”,而是“执行不会运行,直到它被显式生成(例如tokio::run )”

在调用者等待 FooAsync 的返回值之前什么都不会发生,此时 FooAsync 的主体会运行直到等待或返回。

哎呀。 这似乎很不幸。 很多时候我可能永远不会等待某事(通常是由于取消和与任务组合)。 作为一名开发人员,我仍然希望能够获得早期错误(这是人们希望执行运行到等待的最常见原因之一)。

这意味着我们被困在 a) 类似 C# 生成器的语义上,其中没有代码在调用时运行,或者 b) 类似 Kotlin 协程的语义,其中调用函数也立即隐式地等待它(使用类似闭包的 async { . .} 块用于当您确实需要并发执行时)。

鉴于这些,我更喜欢前者而不是后者。 只是我的个人喜好。 如果 kotlin 方法对您的领域感觉更自然,那就去吧!

@CyrusNajmabadi Ick。 这似乎很不幸。 很多时候我可能永远不会等待某事(通常是由于取消和与任务组合)。 作为一名开发人员,我仍然希望能够获得早期错误(这是人们希望执行运行到等待的最常见原因之一)。

我的感觉正好相反。 根据我使用 JavaScript 的经验,忘记使用await是很常见的。 在这种情况下, Promise仍将运行,但错误将被吞下(或发生其他奇怪的事情)。

对于 Rust/Haskell/F# 风格, Future要么运行(正确的错误处理),要么根本不运行。 然后你注意到它没有运行,所以你调查并修复它。 我相信这会产生更健壮的代码。

@Pauan @rpjohnst感谢您的解释。 这些也是我们考虑过的方法。 但事实证明,实际上并没有那么理想。

在您不希望它“实际上做任何事情。您必须手动生成它”的情况下,我们发现将其建模为返回按需生成任务的东西会更清晰。 即像Func<Task>一样简单的东西。

我的感觉正好相反。 根据我使用 JavaScript 的经验,忘记使用 await 是很常见的。

C# 确实努力确保您等待或以其他方式明智地使用任务。

但错误会被吞噬

这和我说的正好相反。 我是说我希望代码能够立即执行,这样我就会立即遇到错误,即使我最终没有时间去执行任务中的代码。 这与迭代器相同。 我更愿意知道我在调用函数的时间点创建它是不正确的,而不是在迭代器流式传输时可能更远。

然后你注意到它没有运行,所以你调查并修复它。

在我所说的场景中,“不运行”是完全合理的。 毕竟,我的应用程序可能随时决定不需要实际运行任务。 这不是我描述的错误。 我所描述的错误是我没有通过验证,我想在我逻辑上创建工作的点而不是工作实际需要运行的点附近找出这一点。 鉴于这些是描述异步处理的模型,通常情况下它们彼此远离。 因此,尽早获得有关问题的信息是很有价值的。

如前所述,这也不是假设。 流/迭代器也会发生类似的事情。 人们经常创造它们,但直到后来才意识到它们。 人们必须将这些东西追溯到源头,这是一种额外的负担。 这就是为什么这么多 API(包括 hte BCL)现在必须在同步/早期工作和实际延迟/延迟工作之间进行拆分。

这和我说的正好相反。 我是说我希望代码能够立即执行,这样我就会立即遇到错误,即使我最终没有时间去执行任务中的代码。

我可以理解对早期错误的渴望,但我很困惑:在什么情况下你会“最终无法产生Future ”?

Future在 Rust 中的工作方式是您以各种方式(包括 async/await,包括并行组合器等)将Future组合在一起,并通过这样做构建一个单个融合Future包含所有子Future s。 然后在程序的顶层( main ),然后使用tokio::run (或类似的)来生成它。

除此之外单tokio::run呼叫main ,你通常不会产卵Future手动s和代替你只是撰写他们。 并且该组合自然会处理生成/错误处理/取消等。 正确。

我也想说清楚。 当我说这样的话:

但事实证明,实际上并没有那么理想。

我在谈论我们的语言/平台的事情。 我只能深入了解对 C#/.Net/CoreFx 等有意义的决定。这可能完全是因为您的情况不同,您想要优化的内容以及您应该采取的方法类型完全不同不同的方向。

我可以理解对早期错误的渴望,但我很困惑:在什么情况下你会“最终无法产生未来”?

每时每刻 :)

考虑一下 Roslyn(C#/VB 编译器/IDE 代码库)本身是如何编写的。 它是高度异步和交互式的。 即它的主要用例是以共享方式与许多访问它的客户端一起使用。 客户端服务通常通过丰富的功能与用户交互,其中许多功能决定不再需要做他们原本认为重要的工作,因为用户执行了任意数量的操作。 例如,当用户打字时,我们正在做大量的任务组合和操作,我们可能最终决定甚至不去执行它们,因为另一个事件在几毫秒后发生。

例如,当用户打字时,我们正在做大量的任务组合和操作,我们可能最终决定甚至不去执行它们,因为另一个事件在几毫秒后发生。

不过,这不只是通过取消处理吗?

并且该组合自然会处理生成/错误处理/取消等。 正确。

听起来我们有两种截然不同的模型来表示事物。 那很好:) 我的解释是在我们选择的模型的上下文中进行的。 它们可能对您选择的模型没有意义。

听起来我们有两种截然不同的模型来表示事物。 那很好:) 我的解释是在我们选择的模型的上下文中进行的。 它们可能对您选择的模型没有意义。

当然,我只是想了解您的观点,并解释我们的观点。 感谢您花时间解释事情。

不过,这不只是通过取消处理吗?

取消是异步的正交概念(对我们而言)。 它们通常一起使用。 但两者都不需要另一个。

您可以拥有一个完全没有取消的系统,并且可能只是您永远不会花时间运行“等待”您编写的任务的代码的情况。 即出于逻辑原因,您的代码可能只是“我不需要等待 't',我只是要做其他事情”。 任务(在我们的世界中)没有任何规定或要求应该等待该任务。 在这样的系统中,我希望获得早期验证。

注意:这类似于迭代器问题。 您可以打电话给某人以获取您打算稍后在代码中使用的结果。 但是,出于多种原因,您最终可能不必实际使用这些结果。 我个人的愿望仍然是尽早获得验证结果,即使我在技术上无法获得它们并且我的程序成功了。

我认为这两个方向都有合理的论据。 但我的看法是,同步方法的优点多于缺点。 当然,如果 hte 同步方法实际上不适合您的实际 impl 想要如何工作,那么这似乎回答了您需要做什么的问题:D

换句话说,我不认为你的方法在这里不好。 如果它在你认为适合 Rust 的模型周围有很大的好处,那就去吧:)

您可以拥有一个完全没有取消的系统,并且可能只是您永远不会花时间运行“等待”您编写的任务的代码的情况。 即出于逻辑原因,您的代码可能只是“我不需要等待 't',我只是要做其他事情”。

就个人而言,我认为最好通过通常的if/then/else逻辑来处理:

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

但正如你所说,它与 C# 只是一个非常不同的观点。

就个人而言,我认为最好通过通常的 if/then/else 逻辑来处理:

是的。 如果可以在创建任务的同一点完成条件检查(并且大量案例都是这样),那就没问题了。 但在我们的世界中,事情通常不会像那样联系得如此紧密。 毕竟,我们想急切地做异步工作以响应用户(以便在需要时准备好结果),但我们可能稍后决定不再关心。

在我们的领域中,“等待”发生在人“需要价值”的时候,这是一个不同的决定/组成部分/等。 从关于“我应该开始研究价值吗?”的决定

从某种意义上说,这些是非常脱钩的,这被视为一种美德。 生产者和消费者可以有完全不同的策略,但可以通过“任务”的良好抽象就正在完成的异步工作进行有效的沟通。

Anways,我会退出同步/异步意见。 显然,这里有非常不同的模型在起作用。 :)

就优先级而言,我已经提供了一些关于 C# 如何看待事物的信息。 我希望它有帮助。 如果您想了解更多信息,请告诉我。

@CyrusNajmabadi是的,您的见解很有帮助。 我个人同意await? foo是可行的方法(尽管我也喜欢“明确的async ”提案)。

顺便说一句,如果您想要关于 .net 模型围绕异步/同步工作建模的所有复杂性以及该系统的所有优点/缺点的最佳专家意见之一,那么@stephentoub将是您要交谈的人。 他在解释事物、阐明利弊以及可能深入研究双方的模型方面比我好 100 倍。 他非常熟悉 .net 在这里的方法(包括做出的选择和拒绝的选择),以及它从一开始就如何发展。 他也痛苦地意识到 .net 所采取的方法的性能成本(这是 ValueTask 现在存在的原因之一),我想这将是你们首先考虑的事情,因为你对零/低的渴望-成本抽象。

根据我的记忆,在早期,.net 的方法中也包含了关于这些拆分的类似想法,我认为他可以很好地说明做出的最终决定以及它们的适当性。

即使看起来有点陌生,我仍然会投票支持await? future 。 编写这些有什么真正的缺点吗?

这是对冷(F#)与热(C#,JS)异步的利弊的另一个彻底分析: http ://tomasp.net/blog/async-csharp-differences.aspx

现在有一个用于后缀宏的新 RFC,它允许在没有专门的语法更改的情况下对后缀await进行实验: https :

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 trait 和Poll枚举名称非常相似,但它们没有关系。 我建议重命名UnsafePoll ,例如重命名UnsafeTask
  • [x] 在 futures crate 中,代码被分成不同的子模块。 现在,大多数代码都集中在task.rs ,这使得导航变得更加困难。 建议再拆分一下。
  • [x] TaskObj#from_poll_task()有一个奇怪的名字。 我建议将其命名为new()
  • [x] TaskObj#poll_task可能只是poll() 。 名为poll的字段可以称为poll_fn ,这也表明它是一个函数指针
  • Waker可能可以使用与TaskObj相同的策略并将 vtable 放在堆栈上。 只是一个想法,我不知道我们是否想要这个。 它会更快,因为它的间接性少一点吗?
  • [ ] dyn现在在测试版中是稳定的。 代码应该在它适用的地方使用dyn

我也可以为这些东西提供 PR。 @cramertj @aturon随时通过 Discord 与我联系以讨论细节。

为所有Future添加一个方法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 在其他语言中不会以这种方式工作),并且可能是编译器中的一个丑陋的 hack。

@elszben编译器可以为所欲为并不意味着它应该为所欲为。

future.await()听起来像一个普通的函数调用,但它不是。 如果你想这样做,上面某处提出的future.await!()语法将允许相同的语义,并用宏清楚地标记“这里发生了一些奇怪的事情,我知道。”

编辑:帖子已删除

我把这篇文章移到期货 RFC 中。 关联

有没有人看过async fn#[must_use]之间的交互?

如果您有async fn ,则直接调用它不会运行任何代码并返回Future ; 似乎所有async fn都应该在“外部” impl Future类型上具有固有的#[must_use] ,因此您不能在不使用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。

我在想异步函数是如何实现的,并意识到这些函数不支持递归,也不支持相互递归。

对于 await 语法,我个人倾向于后修复宏,没有隐式等待方法,因为它易于链接,并且它也可以像方法调用一样使用

@warlord500你完全忽略了上述数百万开发者的全部经验。 你不想链接await的。

@Pzixel请不要假设我没有阅读该主题或我想要的内容。
我知道有些贡献者可能不想允许链接等待,但我们中的一些人
做的开发人员。 我不确定你从哪里得到我忽略的概念
开发人员的意见,我的意见只是说明了社区成员的意见以及我持有该意见的原因。

编辑:如果您有不同意见,请分享! 我很好奇你为什么这么说
我们不应该允许通过像语法这样的方法链接等待?

@warlord500因为 MS 团队在数千名客户和数百万开发人员之间分享了其经验。 我自己知道这一点,因为我每天都在编写 async/await 代码,而且您永远不想链接它们。 如果您愿意,这是确切的报价:

我们对“await”考虑了很多优先级,在设置我们想要的表格之前,我们尝试了很多表格。 我们发现的核心问题之一是,对于我们以及想要使用此功能的客户(内部和外部)而言,人们很少真正想将任何东西“链接”到他们的异步调用之后。 换句话说,人们似乎强烈倾向于“等待”是任何完整表达中最重要的部分,因此它靠近顶部。 注意:“完整表达式”是指您在表达式语句顶部得到的表达式,或顶级赋值右侧的 hte 表达式,或者作为“参数”传递给某物的表达式。

人们想要在 expr 中使用“await”来“继续”的倾向是罕见的。 我们偶尔会看到诸如(await expr).M() ,但与执行await expr.M()的人数相比,这些似乎不太常见,也不太理想。

我现在很困惑,如果我理解正确,我们不应该支持
等待 post-fix 简单链样式,因为它不常用? 您认为 await 是表达式中最重要的部分。
在这种情况下,我只是假设我能正确理解你。
如果我错了,请不要犹豫纠正我。

另外,您可以将链接发布到您获得报价的位置,
谢谢。

我反对以上两点只是因为您不常用某些东西,并不一定意味着支持它对于使代码更清晰的情况有害。

有时 await inst 表达式中最重要的部分,如果未来生成的表达式是
最重要的部分,你想把它放在顶部,如果我们允许除了普通宏样式之外的后缀宏样式,你仍然可以这样做

另外,您可以将链接发布到您获得报价的位置,
谢谢。

但是...但是你说你已经阅读了整篇文章...😃

但我分享它没有问题: https :

有时 await inst 表达式中最重要的部分

引用显然是相反的 😄 而且你知道,我自己也有同样的感觉,每天都在编写 async/await。

你有使用 async/await 的经验吗? 那你可以分享一下吗?

哇,我不敢相信我错过了。 感谢您抽出时间来链接它。
我没有任何经验,所以我想在宏伟的计划中,我的意见并不那么重要

@Pzixel感谢您分享有关您和其他人使用async / await的体验的信息,但请尊重其他贡献者。 你不需要为了让自己的技术点被听到而批评别人的经验水平。

版主注: @Pzixel不允许对社区成员进行人身攻击。 我已经从你的评论中编辑了它。 不要再这样做了。 如果您对我们的审核政策有任何疑问,请通过[email protected]与我们

@crabtw我没有批评任何人。 对于可能在这里造成的任何不便,我深表歉意。

当我想了解人们是否真的需要链接“await”或者是他对今天功能的推断时,我曾询问过体验。 我不想诉诸权威,它只是一堆有用的信息,我可以说“你需要自己尝试并自己意识到这个事实”。 这里没有任何攻击性。

不允许对社区成员进行人身攻击。 我已经从你的评论中编辑了它。

没有人身攻击。 正如我所看到的,你评论了我关于downvotes的参考。 好吧,这只是我在投反对票后的反应堆,没什么特别的。 由于它已被删除,因此删除该引用也是合理的(它甚至可能会让更多读者感到困惑),因此感谢您将其删除。

谢谢参考。 我确实想提一下,您不应该接受我所说的任何“福音”:) Rust 和 C# 是具有不同社区、范式和习语的不同语言。 你应该为你的语言做出最好的选择。 我确实希望我的话是有帮助的,可以提供洞察力。 但始终对不同的做事方式持开放态度。

我希望你为 Rust 想出一些惊人的东西。 然后我们就可以看到你做了什么,欣然采纳了C#:)

据我所知,链接的论点主要讨论了await优先级,特别是认为将await x.y()解析为await (x.y())而不是(await x).y()是有意义的await!(x.y())这样的语法消除了歧义。

但是,我不认为这对像x.y().await!().z()这样的链接的价值提出了任何特定的答案。

引用的评论很有趣,部分原因是 Rust 有很大的不同,这是延迟我们找出最终 await 语法的重要因素之一: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

我在 2743 sloc 中有 139 个等待。 也许这没什么大不了的,但我认为我们应该考虑将无支架替代品视为更清洁、更好的替代品。 话虽如此, ?是唯一的例外,因此我们可以轻松地使用不带大括号的await foo ,并为这种特殊情况引入特殊语法。 这没什么大不了的,但可以为 LISP 项目节省一些大括号。

我已经创建了一篇关于为什么我认为异步函数应该使用外部返回类型方法作为其签名的博客文章。 享受阅读!

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

我还没有关注所有的讨论,所以如果我错过了它,请随时指出我已经讨论过的地方。

这里有一个关于内部返回类型方法的额外问题: Stream的语法在何时被指定? 我认为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;  ... }

注意:生成器已经在 nightly 中出现了,但它们不使用这种语法。 与期货 0.3 中的Stream不同,它们目前也不是固定感知的。

编辑:此代码以前使用了Generator 。 我错过了StreamGenerator之间的区别。 流是异步的。 这意味着它们可能但不必产生值。 他们可以回复Poll::ReadyPoll::Pending 。 另一方面, Generator必须始终同步产生或完成。 我现在已将其更改为AsyncGenerator以反映这一点。

Edit2: @Ekleog生成器的当前实现使用没有标记的语法,并且似乎通过在正文中查找yield来检测它应该是生成器。 这意味着您说async可以重用是正确的。 不过,这种方法是否明智是另一个问题。 但我想那是另一个话题^^'

确实,我在想async可以重用,这仅仅是因为async根据这个 RFC,只允许与Future ,因此可以检测它产生Stream通过查看返回类型(它必须是FutureStream )。

我现在提出这个的原因是因为如果我们想要使用相同的async关键字来生成Future s 和Stream s,那么我认为外部返回类型方法会更清晰,因为它是明确的,我认为没有人会期望async fn foo() -> i32会产生i32 (如果主体包含yield并选择了内部返回类型方法)。

我们可以为生成器设置第二个关键字(例如gen fn ),然后仅通过应用这两个关键字来创建流(例如async gen fn )。 外部返回类型根本不需要涉及。

@rpjohnst我提出它是因为外部返回类型方法可以轻松设置两个关联类型。

我们不想设置两个关联类型。 Stream 仍然只是一种类型,而不是impl Iterator<Item=impl Future>>或类似的东西。

@rpjohnst我的意思是(异步)生成器的关联类型YieldReturn

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 语法的稳定性将等待异步流的体面语法和实现,并获得他们两个的经验? (因为一旦稳定,就不可能在外部返回类型和内部返回类型之间进行更改)

我认为 Rust 2018 预计会出现 async/await 并且不希望到那时异步生成器准备就绪,但是......?

(此外,我的评论只是作为@MajorBreakfast博客文章的补充论据,但它似乎完全消除了对该主题的讨论……这根本不是我的目标,我想辩论应该重新集中在这篇博文?)

await 关键字的狭隘用例仍然让我感到困惑。 (尤其是 Future vs Stream vs Generator)

对于所有用例,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怎么样?
我比mut let更喜欢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 ,表示你在搜索公共异步函数。

@张汉东

  • async fn定义了一个返回未来的普通函数。 所有返回未来的函数都被认为是“异步的”。 async fn s 的函数指针和其他返回future 的函数是相同的。 这是一个游乐场示例。 搜索“async fn”只能找到使用该符号的函数,它不会找到所有异步函数。
  • 搜索pub fn不会找到unsafeconst函数。

° async fn返回的具体类型当然是匿名的。 我的意思是他们都返回一个实现Future

@xmeta注意mut不会“让”,或者更确切地说, mut不会修改letlet需要一个模式,即

let PATTERN = EXPRESSION;

mutPATTERN ,而不是let本身的一部分。 例如:

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

@steveklabnik我明白。 我只是想展示层次结构和词序之间的关联。 谢谢

人们对async块内returnbreak的理想行为有何看法? 目前return从异步块返回——如果我们完全允许return ,这真的是唯一可能的选择。 我们可以彻底禁止return并使用类似'label: async { .... break 'label x; }东西从异步块返回。 这也与关于是否使用关键字breakreturn用于 break-to-blocks 功能的对话有关(https://github.com/rust-lang/rust/issues/ 48594)。

我支持return 。 禁止它的主要问题是它可能会令人困惑,因为它不是从当前函数返回,而是从异步块返回。 但是,我怀疑它会令人困惑。 闭包已经允许return并且我从未发现它令人困惑。 了解return也适用于异步块在 IMO 中很容易,并且允许它在 IMO 中非常有价值。

@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与闭包对齐,并且看起来很容易理解和解释。

break-to-block rfc 是否计划允许跳出带有标签的内部闭合? 如果没有(并且我不建议它应该允许它),那么禁止returns的一致行为将是非常不幸的,然后使用与 break-to-blocks 的 rfc 也不一致的替代方案。

@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 _without_ 提供标签的异步块(没有办法将异步块跳出到外部循环,因此您不会失去任何表现力)。

@withoutboats闭包只是另一种函数; 一旦你学会了“闭包是一个函数”,那么你就可以将你所知道的关于函数的一切应用到闭包中,包括“ return总是从包含函数返回”。

@Nemo157即使您没有标记breakasyncbreak目标,您也必须提供一种机制(如'label: async )从异步块内的循环中提前返回.

@joshtriplett

闭包只是另一种函数; 一旦你学会了“闭包是一个函数”,那么你就可以将你所知道的关于函数的一切应用到闭包中,包括“返回总是从包含函数返回”。

我认为async块也是一种“函数”——没有参数可以异步运行完成。 它们是async闭包的特例,没有参数并且已经预先应用。

@cramertj是的,我假设任何隐式断点也可以在必要时标记(因为我相信它们目前都可以)。

任何使控制流更难遵循的东西,特别是重新定义return含义,都会对流畅阅读代码的能力造成很大压力。

同样,C 中的标准指导是“不要编写从宏中间返回的宏”。 或者,作为一个不太常见但仍然存在问题的情况:如果您编写一个看起来像循环的宏, breakcontinue应该在其中工作。 我见过人们编写实际上嵌入两个循环的循环宏,因此break无法按预期工作,这非常令人困惑。

我认为异步块也是一种“功能”

我认为这是一种基于了解实现内部原理的观点。

我根本不认为它们是函数。

我根本不认为它们是函数。

@joshtriplett

我怀疑您第一次对带有闭包的语言提出了同样的论点—— return不应该在闭包内工作,而应该在定义函数内工作。 事实上,有些语言采用了这种解释,比如 Scala。

@cramertj我不会,不; 对于在函数中定义的 lambda 和/或函数,感觉它们是函数是完全自然的。 (我第一次接触这些是在 Python、FWIW 中,其中 lambdas 不能使用return并且在嵌套函数中return从包含return的函数返回。)

我认为一旦知道 async 块的作用, return必须如何表现就很直观了。 一旦您知道它表示延迟执行,很明显return不能应用于该函数。 很明显,该函数在块运行时已经返回。 海事组织学习这不应该是一个很大的挑战。 我们至少应该尝试一下,看看。

该 RFC 没有提出? -operator 和控制流结构如returnbreakcontinue应该如何在异步块中工作。

在编写专用 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 中。 我建议在可以将其放入 libcore 后将其设为公共 API。

我怀疑是否有可能有一个可以从非async函数调用的函数,一旦它从 TLS 中移出,它就可以为您提供来自某个父async函数的上下文。 那时,上下文可能会被视为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我认为你是对的:如果不是直接从异步 fn 中调用,我提议的函数可能无法工作。 也许最好的方法是让spawn_with_handle成为一个在内部使用await spawn_with_handle的宏(如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是其他语言的知名关键字吗? @可能很有趣,因为它类似于 await 中的a ,但它可能是任何适合的符号。

我认为这样的语法选择是合乎逻辑的,因为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还是一个符号,为什么? 我是否因为讨论这个而疯狂,或者我错过了什么?

对于我可能使用的任何不正确的术语,我们深表歉意。

我对基于 sigil 的语法最大的担忧是它很容易变成字形汤,正如你所展示的那样。 任何熟悉 Perl(6 岁之前)的人都会明白我要做什么。 避免线路噪音是前进的最佳方式。

也就是说,也许最好的方法实际上与try!完全一样? 也就是说,从一个显式的async!(foo)宏开始,如果需要的话,添加一些符号来作为它的糖。 当然,这将问题推迟到以后,但async!(foo)足以用于 async/await 的第一次迭代,其优点是相对没有争议。 (并且有try! / ?的先例应该需要出现一个印记)

@withoutboats我还没有阅读整篇文章,但是有人帮助实现吗? 你的开发分支在哪里?

而且,关于剩下的未解决的问题,有没有人向 Rust 社区以外的专家寻求帮助? Joe Duffy非常了解并发性,并且非常了解复杂的细节,他在 RustConf 上发表

@BatmanAoD初始实现已登陆https://github.com/rust-lang/rust/pull/51580

最初的 RFC 线程有来自 PLT 领域的许多专家的评论,甚至在 Rust 之外:)

我想建议等待期货的“$”符号,因为时间就是金钱,我想提醒编译器这一点。

开玩笑而已。 我不认为有一个等待的符号是个好主意。 Rust 就是要明确,让人们能够用一种强大的语言编写低级代码,而不会让你在脚下开枪。 符号比await!宏要模糊得多,它允许人们通过编写难以阅读的代码以不同的方式射击自己。 我已经会争辩说?是一步太远了。

我也不同意以async fn形式使用async关键字。 它意味着对异步的某种“偏见”。 为什么 async 值得一个关键字? 异步代码对我们来说只是另一个抽象,并不总是必要的。 我认为 async 属性更像是基本 Rust 方言的“扩展”,它允许我们编写更强大的代码。

我不是语言架构师,但我有一些在 JavaScript 中使用 Promise 编写异步代码的经验,我认为它在那里完成的方式使编写异步代码成为一种乐趣。

@steveklabnik啊,好的,谢谢。 我们可以(/我应该)更新问题描述吗? 也许项目符号“初始实现”应该拆分为“没有move支持的实现”和“完全实现”?

下一个实现迭代是否正在某个公共分支/分支中进行? 或者在 RFC 2418 被接受之前甚至不能继续进行?

为什么在此处而不是在 RFC 中讨论 async/await 语法问题?

@c-edw 我认为有关async关键字的问题由您的函数是什么颜色回答的

@parasyte有人向我建议,该帖子实际上是反对没有自动管理的绿色线程样式并发的异步函数的整个想法的论据。

我不同意这个立场,因为如果没有(托管)运行时,绿色线程就无法透明地实现,并且 Rust 有充分的理由支持异步代码而不需要它。

但是看你的帖子好像是async / await语义没问题,但是对关键字有结论吗? 你介意扩展一下吗?

我也同意你的观点。 我评论说async关键字是必要的,文章列出了它背后的原因。 作者得出的结论是另一回事。

@parasyte啊,好吧。 很高兴我问了——因为作者厌恶红/蓝二分法,我以为你说的是​​相反的!

我想进一步澄清,因为我觉得我没有做到这一点。

这种二分法是不可避免的。 一些项目试图通过使每个函数调用异步来消除它,强制同步函数调用不存在。 Midori 就是一个明显的例子。 其他项目试图通过在同步函数的外观后面隐藏异步函数来消除二分法。 gevent 就是这样的一个例子。

两者都有相同的问题; 他们仍然需要二分法来区分等待异步任务完成异步启动任务

  • Midori 不仅引入了await关键字,还在函数调用站点上引入了async关键字。
  • 除了外观正常的函数调用的隐式等待之外,gevent 还提供gevent.spawn

这就是我提出颜色函数文章的全部原因,因为它回答了“为什么 async 需要一个关键字?”的问题。

好吧,即使是基于线程的同步代码也可以区分“等待任务完成”(join)和“开始任务”(spawn)。 您可以想象一种语言,其中一切都是异步的(实现方面),但是await上没有注释(因为它是默认行为),而 Midori 的async是传递给spawn的闭包

所以虽然我同意 async 应该有一个关键字,但在我看来这更多是因为 Rust 关心这个级别的实现机制,因此需要提供两种颜色。

@rpjohnst是的,我已经阅读了您的建议。 它在概念上与隐藏颜色 a la gevent 相同。 我在 Rust 论坛上的同一个帖子中批评了它; 每个函数调用看起来都是同步的,当一个函数在异步管道中既同步又阻塞时,这是一个特殊的危险。 这种错误是不可预测的,并且是解决故障的真正灾难。

我不是特别在谈论我的提案,我在谈论一种一切都是异步的语言。 你可以通过这种方式摆脱二分法——我的提议并没有尝试这样做。

IIUC 这正是 Midori 所尝试的。 在这一点上,关键字与闭包只是在争论语义。

2018 年 7 月 12 日星期四下午 3:01,Russell Johnston通知@github.com
写道:

您使用关键字的存在作为二分法的论据
仍然存在于Midori。 如果你去掉它们,二分法在哪里? 这
语法与所有同步代码相同,但具有异步功能
代码。

因为当你调用一个异步函数而不等待它的结果时,它
同步返回一个承诺。 可以等待稍后。 😐

_哇,有人知道Midori吗? 我一直认为这是一个封闭的项目,几乎没有活的生物在做。 如果你们中有人更详细地写过它会很有趣。_

/无关

@Pzixel没有生物仍在研究它,因为该项目已关闭。 但是 Joe Duffy 的博客有很多有趣的细节。 见我上面的链接。

我们在这里偏离了轨道,我觉得我在重复自己,但这是“关键字的存在”的一部分 - await关键字。 如果您用spawnjoin等 API 替换关键字,您可以完全异步(如 Midori),但没有任何二分法(与 Midori 不同)。

或者换句话说,就像我之前说的,这不是基本的——我们拥有它只是因为我们想要选择。

@CyrusNajmabadi很抱歉再次给你这里有一些关于决策的额外信息。

如果你不想再被提及,请告诉我。 我只是觉得你可能会感兴趣。

来自#wg-net discord 频道

@cramertj
深思熟虑:我经常在async { ... }块的末尾写Ok::<(), MyErrorType>(()) 。 也许我们可以想出一些办法来更容易地限制错误类型?

@没有船
[...] 可能我们希望它与 [ 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 { ... };

我之前曾考虑过允许Future trait 使用类似 fn 的语法的想法,例如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<()>做了 return-type-desugaring ,所以 IMO 如果我们使用基于->的语法,很明显我们会在这里想要相同的糖。

@cramertj是的,它应该是一致的。 我上面的帖子有点假设我可以说服你们所有人,外部返回类型方法是优越的 😁

如果我们使用async -> R { .. }那么大概我们也应该使用try -> R { .. }以及通常使用expr -> TheType作为类型归属。 换句话说,我们使用的类型归属语法应该在任何地方统一应用。

@Centril我同意。 它应该可以在任何地方使用。 我只是不确定->是否真的是正确的选择。 我将->与可调用联系起来。 异步块不是。

@MajorBreakfast我基本同意; 我认为我们应该使用:作为类型归属,所以async : Type { .. }try : Type { .. }expr : Type 。 我们已经讨论了 Discord 上潜在的歧义,我认为我们找到了一个前进的方向, :是有意义的......

另一个问题是关于Either枚举。 我们已经在futures板条箱中有Either 。 它也被迷惑,因为它看起来就像Eithereither箱子时,它不是。

因为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函数中的Either ,只要你await期货那么async所做的代码转换就会隐藏这两个内部类型并为编译器提供一个单一的返回类型。

@Pzixel另外,我(个人)希望Either不会被引入这个 RFC,因为那会引入一个受限版本的https://github.com/rust-lang/rfcs/issues /2414 (仅适用于 2 种类型且仅适用于Future s),因此如果合并了通用解决方案,则可能会添加 API cruft——正如Either :)

@Ekleog当然,我只是被这个想法either ,我真的很想摆脱它们。 然后,当我花了大约半个小时才想起我的困惑,直到我意识到它无法编译,因为我在依赖项中有either板条箱(未来的错误很难理解,所以花了很长时间)。 所以这就是我写评论的原因,只是为了确保这个问题以某种方式得到解决。

当然,这不仅仅与async/await ,它是更通用的东西,因此它值得拥有自己的 RFC。 我只想强调futures应该知道either或反之亦然(为了正确实现IntoFuture )。

@Pzixel期货板条箱导出的Either是从either板条箱重新导出的。 由于孤儿规则, futures crate 0.3 无法为Either实现Future 。 为了保持一致性,我们很可能还将删除StreamSink impls 以实现Either并提供替代方案(在此处讨论)。 此外, either板条箱然后可以实现FutureStreamSink本身,可能在功能标志下。

也就是说,正如@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,我想应该将其添加到最顶层帖子的跟踪列表中:)

这是一个操场,展示了实现Send _should_ 的 async/await 函数。 取消注释Rc版本正确地将该函数检测为非Send 。 我可以稍微看一下你的具体例子(这台机器上没有 Rust 编译器:slightly_frowning_face :) 尝试找出它不工作的原因。

@Ekleog std::mpsc::Receiver不是Sync ,您编写的async fn包含对它的引用。 对!Sync项目的引用是!Send

@cramertj嗯……但是,我是不是持有一个拥有的mpsc::Receiver ,如果它的泛型类型是Send ,它应该是Send Send吗? (另外,它不是std::mpsc::Receiver而是futures::channel::mpsc::Receiver ,如果类型是Send ,它也是Sync Send ,抱歉没有注意到mpsc::Receiver别名不明确!)

@Nemo157谢谢! 我打开了 https://github.com/rust-lang/rust/issues/53259 以避免在这个问题上有太多的噪音:)

async块是否以及如何允许?和其他控制流的问题可能需要与try块进行一些交互(例如try async { .. }允许?没有与return类似的混淆?)。

这意味着指定async块类型的机制可能需要与指定try块类型的机制进行交互。 我对归属语法 RFC 发表了评论: https :

刚开始我认为是futures-rs问题,但事实证明它实际上可能是 async/await 问题,所以这里是: https :

一个新问题: https :

正如几天前在 discord 上讨论的那样, await还没有被保留为关键字。 在 2018 年发布之前获得此保留(并添加到 2018 版关键字 lint)非常重要。 这是一个稍微复杂的保留,因为我们现在想继续使用宏语法。

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

注意。 这个跟踪问题的名称是 async/await 但它也分配给了任务 API! 任务 API 目前有一个稳定的 RFC 待定: https :

有机会使关键字可重用于替代异步实现吗? 目前它创建了一个 Future,但它错过了使基于推送的异步更有用的机会。

@aep使用oneshot::channel可以轻松地从基于推送的系统转换为基于拉取的 Future 系统。

例如,JavaScript Promises 是基于推送的,因此 stdweb使用oneshot::channel将 JavaScript Promises 转换为 Rust Futures 。 它还将oneshot::channel用于其他一些基于推送的回调 API,例如setTimeout

由于 Rust 的内存模型,与 pull 相比oneshot::channel ),而不是让整个 Future 系统都基于推送。

话虽如此,我不是核心或 lang 团队的一员,所以我说的任何话都没有任何权威。 这只是我个人的意见。

它实际上是资源受限代码的另一种方式。 拉模型有一个惩罚,因为您需要被拉的事物内部的资源,而不是通过一堆等待函数提供下一个就绪值。 futures.rs 的设计对于任何接近硬件的东西来说都太昂贵了,比如网络交换机(我的用例)或游戏渲染器。

然而,在这种情况下,我们只需要像 Generator 那样使 async 发出一些东西。 正如我之前所说,如果你充分抽象异步和生成器而不是将两个关键字绑定到单个库,我认为它实际上是一回事。

然而,在这种情况下,我们只需要像 Generator 那样使 async 发出一些东西。

async在这一点上实际上是一个围绕生成器文字的最小包装器。 我很难看到生成器如何帮助基于推送的异步 IO,您难道不想为那些需要 CPS 转换吗?

您能否更具体地说明“您需要被拉动的事物内部的资源”的意思? 我不确定您为什么需要它,或者“通过一堆等待函数提供下一个就绪值”与poll()有何不同。

我的印象是基于推送的期货更昂贵(因此更难在受限环境中使用)。 允许将任意回调附加到未来需要某种形式的间接,通常是堆分配,因此不是在每个组合子上分配根未来分配一次。 由于线程安全问题,取消也变得更加复杂,因此您要么不支持它,要么需要所有回调完成以使用原子操作来避免竞争。 据我所知,所有这些加起来使优化框架变得更加困难。

你不是更需要一个 CPS 转换吗?

是的,当前的生成器语法对此不起作用,因为它没有延续的参数,这就是为什么我希望 async 能带来实现它的方法。

你需要被拉动的东西里面的资源吗?

这是我所说的颠倒顺序异步工作两次是有成本的可怕方式。 即一次从硬件到期货,然后再次使用渠道返回。 您需要携带一大堆在近硬件代码中具有零优势的东西。

一个常见的例子是,当您知道套接字的文件描述符已准备好时,您不能只调用 future 堆栈,而是必须实现所有执行逻辑以将现实世界的事件映射到 future,这具有外部成本,例如锁定,代码大小和最重要的代码复杂度。

允许将任意回调附加到未来需要某种形式的间接

是的,我知道回调在某些环境中很昂贵(不是在我的环境中,执行速度无关紧要,但我有 1MB 的总内存,所以 futures.rs 甚至不适合闪存),但是,当你有时,你根本不需要动态调度类似于延续(当前生成器概念的一半实现)。

由于线程安全,取消也变得更加复杂

我认为我们在这里混淆了事情。 我不提倡回调。 延续可以是静态堆栈就好了。 例如,我们在粘土语言中实现的只是一个可以用于推或拉的生成器模式。 IE:

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

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

我想我可以继续用宏来做这件事,但我觉得在这里浪费一个特定概念的语言关键字是一个错失的机会。

不是在我的,执行速度无关紧要,但我有 1MB 总内存,所以 futures.rs 甚至不适合闪存

我很确定当前的期货旨在在内存受限的环境中运行。 究竟是什么占用了这么多空间?

编辑:这个程序在我的 macbook 上编译时需要 295KB 磁盘空间(基本 hello world 需要 273KB):

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

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

不是在我的,执行速度无关紧要,但我有 1MB 总内存,所以 futures.rs 甚至不适合闪存

我很确定当前的期货旨在在内存受限的环境中运行。 究竟是什么占用了这么多空间?

还有你说的内存是什么意思? 我已经在具有 128 kB 闪存/16 kB ram 的设备上运行了当前基于异步/等待的代码。 目前 async/await 肯定存在内存使用问题,但这些主要是实现问题,可以通过添加一些额外的优化来改进(例如 https://github.com/rust-lang/rust/issues/52924)。

一个常见的例子是,当您知道套接字的文件描述符已准备好时,您不能只调用 future 堆栈,而是必须实现所有执行逻辑以将现实世界的事件映射到 future,这具有外部成本,例如锁定,代码大小和最重要的代码复杂度。

为什么? 这似乎仍然不像是期货强迫你做的任何事情。 您可以像调用基于推送的机制一样轻松地调用poll

还有你说的内存是什么意思?

我不认为这是相关的。 整个讨论已经详细说明了我什至不想提出的观点。 除了说将其设计融入核心语言是一个错误之外,我不是在这里批评期货。

我的观点是 async 关键字如果做得好,可以在未来证明。 延续是我想要的,但也许其他人会提出更好的想法。

您可以像调用基于推送的机制一样轻松地调用 poll。

是的,如果Future:poll有调用 args,那会有意义。 它不能有它们,因为 poll 需要抽象。 相反,我建议从 async 关键字发出一个延续,并为任何带有零参数的延续实现 Future。

这是一种简单、省力的更改,不会增加期货的成本,但允许重复使用目前专用​​于一个库的关键字。

但是 continations 当然也可以用预处理器来实现,这就是我们要做的。 不幸的是,脱糖只能是一个闭包,这比适当的延续更昂贵。

@aep我们如何才能重用关键字( asyncawait )?

@Centril我天真的快速解决方法是将异步降低到生成器而不是未来。 这将使生成器有时间用于适当的延续,而不是成为期货的独家后端。

它可能像一个 10 行的 PR。 但是我没有耐心与蜂巢争夺它,因此我将构建一个预处理程序来对不同的关键字进行脱糖。

我没有关注异步的东西所以很抱歉,如果这已经在之前/其他地方讨论过,但是在no_std支持异步/等待的(实现)计划是什么?

AFAICT 当前的实现使用 TLS 来传递 Waker,但no_std / core没有 TLS(或线程)支持。 我从@alexcrichton听说,如果/当Generator.resume获得对参数的支持时,可能可以摆脱 TLS。

是否正在实施在no_std支持下阻止 async/await 稳定的计划? 或者我们确定可以添加no_std支持而不更改任何将被稳定的部分,以在稳定时发送std异步/等待?

@japaric poll现在明确地接受上下文。 AFAIK,不应再需要 TLS。

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

编辑:与异步/等待无关,仅适用于期货。

[...] 我们确定可以添加no_std支持而不更改任何将被稳定的部分,以在稳定时将std异步/等待稳定发布吗?

我相信是这样。 相关的部分是std::future中的函数,这些都隐藏在一个额外的gen_future不稳定的特性后面,这个特性永远不会稳定。 async转换使用set_task_waker将唤醒器存储到 TLS 中,然后await!使用poll_with_tls_waker来访问它。 如果生成器获得 resume 参数支持,则async转换可以将唤醒器作为 resume 参数传入,而await!可以从参数中读取它。

编辑:即使没有生成器参数,我相信这也可以通过异步转换中的一些稍微复杂的代码来完成。 我个人希望看到为其他用例添加的生成器参数,但我非常确定使用/不使用它们来删除 TLS 要求是可能的。

@japaric同样的船。 即使有人让未来在嵌入式上工作,它也是非常危险的,因为它都是 Tier3。

我发现了一个丑陋的 hack,它需要的工作比修复 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

但如果我是唯一想要它的人,那么一路走下去就毫无意义。

我一直在思考没有生成器参数的 TLS-less async/await 是否可行,所以我实现了一个基于no_std proc-macro 的async_block! / await!宏对只使用局部变量。

与当前基于 TLS 的解决方案或基于生成器参数的解决方案相比,它肯定需要更微妙的安全保证(至少当您假设底层生成器参数是合理的时),但我很确定它是合理的(只要没有人使用了相当大的卫生漏洞,我找不到解决办法,这对于编译器内实现来说不是问题,因为它可以使用无法命名的 gensym 标识在异步转换和等待宏之间进行通信)。

我刚刚意识到在 OP 中没有提到将await!stdcore ,也许可以将 #56767 添加到在稳定跟踪之前要解决的问题列表中这。

@Nemo157由于await!预计不会稳定,因此无论如何它都不是阻滞剂。

@Centril我不知道谁告诉你await!预计不会稳定... :wink:

@cramertj他的意思是宏版本而不是我相信的关键字版本......

@crlf0710隐式等待/显式异步块版本是什么?

@crlf0710我也做了 :)

@cramertj难道我们不想删除宏,因为编译器中目前存在一个丑陋的 hack 使得awaitawait!可能存在吗? 如果我们稳定宏,我们将永远无法删除它。

@stjepang我真的不太关心await!的语法的任何方向,除了对后缀符号的普遍偏好以及不喜欢歧义和不可发音/不可谷歌的符号。 据我所知,目前的建议(用?来澄清优先级)是:

  • await!(x)? (我们今天拥有的)
  • await x?await?绑定更紧密,仍然是前缀符号,需要括号来链接方法)
  • await {x}? (同上,但暂时需要{}以消除歧义)
  • await? xawait绑定不那么紧密,仍然是前缀符号,需要括号来链接方法)
  • x.await? (貌似是字段访问)
  • x# / x~ / 等。 (一些符号)
  • x.await!()? (postfix-macro-style, @withoutboats ,我认为其他人可能不是 postfix-macros 的粉丝,因为他们希望.允许基于类型的调度,而对于 postfix 宏则不会)

我认为运输的最佳途径是登陆await!(x) ,un-keyword-ify await ,最终有一天会向人们推销 postfix 宏的好处,让我们添加x.await!() 。 其他人有不同的意见;)

我非常松散地关注这个问题,但这是我的意见:

我个人喜欢await!宏,正如它在此处描述的那样: https :

它不是任何魔法或新语法,只是一个普通的宏。 毕竟少即是多。

再说一次,我也更喜欢try! ,因为Try仍然不稳定。 然而, await!(x)?是糖和明显命名动作之间的一个不错的折衷,我认为它运作良好。 此外,它可能会被第三方库中的一些其他宏替换,以处理额外的功能,例如调试跟踪。

同时async / yield是生成器的“只是”语法糖。 这让我想起了 JavaScript 获得 async/await 支持的日子,你有像 Babel 和Regenerator这样的项目将异步代码转换为使用生成器和 Promises/Futures 进行异步操作,本质上就像我们正在做的那样。

请记住,最终我们希望 async 和 generators 成为不同的特性,甚至可能相互组合(产生Stream )。 将await!作为一个只降低到yield的宏并不是一个永久的解决方案。

离开等待! 作为一个只是降低产量的宏不是一个永久的解决方案。

它不能永久地对用户可见,它降低到yield ,但它当然可以继续以这种方式实现。 即使你有 async + generators = Stream你仍然可以使用例如yield Poll::Pending; vs. yield Poll::Ready(next_value)

请记住,最终我们希望 async 和 generators 成为不同的功能

异步和生成器不是不同的功能吗? 当然是相关的,但再次将其与 JavaScript 的做法进行比较,我一直认为 async 将构建在生成器之上; 唯一的区别是异步函数将返回并产生Future s,而不是任何常规值。 执行器需要评估并等待异步函数执行。 加上一些我不确定的额外生命周期的东西。

事实上,我曾经写过一个关于这个确切事情

@cramertj如果两者是不同的“效果”,则无法以这种方式实现。 这里有一些讨论: https : yield Poll::Ready(next_value) ,我们想要yield next_value ,并且在同一个函数的其他地方有await s。

@rpjohnst

我们不想产生 Poll::Ready(next_value),我们想要产生 next_value,并在同一函数的其他地方等待。

是的,当然这就是用户看到的样子,但就脱糖而言,您只需要将yield包裹在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 是一种推送模型; 一旦您订阅它,无论您是否准备好,您都会全速收到事件和通知,因此您必须实施缓冲或采样策略来处理闲聊,”Terlson 解释说。 异步迭代器是一个推拉模型——你请求一个值,然后它被发送给你——它更适用于网络 IO 原语之类的东西。

@Centril好的,打开#56974,这是否足够正确,可以作为未解决的问题添加到 OP 中?


我真的不想再次进入await语法自行车棚,但我必须至少回应一点:

就我个人而言,我喜欢await!宏,正如它在此处描述的那样: https :

请注意,我还说过我不相信该宏可以保留为库实现的宏(忽略它是否会继续作为宏向用户显示),以扩展原因:

  1. 隐藏底层实现,正如未解决的问题之一所说,您目前可以使用|| await!()创建一个生成器。
  2. 支持异步生成器,正如@cramertj提到的,这需要区分yield添加的await和用户编写的其他yield 。 这_可以_作为宏扩展前的阶段来完成,_if_用户从不想在宏中使用yield ,但是有非常有用的yield -in-macro 结构,如yield_from! . 由于必须支持宏中的yield s 的约束,这要求await!至少是一个内置宏(如果不是实际语法)。
  3. no_std上支持async fn no_std 。 我知道有两种实现方式,这两种方式都需要async fn -created- Futureawait来共享存储唤醒器的标识符。我唯一的方法如果两者都在编译器中实现,可以看到在这两个地方之间共享一个卫生安全的标识符。

我认为这里有一些混乱—— 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<_,_>>

@地球引擎

虽然我并不热衷于awaited关键字,但我认为您有所了解。

这里的关键见解是: yieldawait就像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;

由于@cramertj刚刚显示的确切原因,我不喜欢后缀等待语法。 它降低了整体可读性,尤其是对于长表达式或链式表达式。 它不会像await! / await那样提供任何嵌套的感觉。 它没有?的简单性,而且我们用完了用于后缀运算符的符号......

就我个人而言,由于我之前描述的原因,我仍然支持await! 。 感觉 Rust-y 和废话。

它降低了整体可读性,尤其是对于长表达式或链式表达式。

在 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?.xfoo await?.y (不否认它们看起来很奇怪,只是认为优先级很明确)。

我也认为

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

读起来比

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

总而言之,我会支持这一点。

@joshtriplett RE .await!()我们应该分开讨论——我最初也赞成这个,但我认为如果我们不能同时获得 postfix 宏,我认为我们不应该这样做,我认为有很多人反对他们(虽然理由很不幸,但理由很充分),我真的希望不要阻止await稳定。

为什么不同时?

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

我现在确实更多地看到了 postfix 的吸引力,并且在某些情况下更接近于喜欢它。 尤其是上面的作弊,它是如此简单,甚至不需要由 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。


对于那些不喜欢中间的空格但可以用于后缀关键字运算符的人,如何使用连字符:

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

关于编写相同代码的许多不同方式,我个人不喜欢它。 主要原因是新手很难正确理解正确的方式或拥有它的意义。 第二个原因是我们将有许多不同的项目,它们使用不同的语法,并且很难在它们之间跳转和阅读(特别是对于 Rust 的新手)。 我认为只有在实际存在差异并且提供一些优势时才应该实施不同的语法。 大量的代码糖只会让语言的学习和工作变得更加困难。

@goffrie是的,我同意我们不应该有很多不同的方法来做同样的事情。 然而我只是提出不同的选择,社区只需要选择一个。 因此,这并不是真正的问题。

此外,就await!宏而言,没有办法阻止用户发明自己的宏来做不同的事情,而 Rust 旨在实现这一点。 因此,“有许多不同的方法来做同样的事情”是不可避免的。

我认为我展示的那个简单的哑宏表明,无论我们做什么,用户都可以为所欲为。 一个关键字,无论是前缀还是后缀,只要存在,就可以被制成类似函数的前缀宏,或者可能是类似后缀方法的宏。 即使我们为await选择了类似函数或方法的宏,它们也可以被另一个宏反转。 真的没关系。

因此,我们应该注重灵活性和格式。 提供最容易满足所有这些可能性的解决方案。

此外,虽然在这么短的时间内我已经越来越喜欢 postfix 关键字语法,但await应该用生成器反映yield任何决定,这可能是一个前缀关键字。 对于需要后缀解决方案的用户,最终可能会存在类似方法的宏。

我的结论是前缀关键字await是目前最好的默认语法,也许常规的 crate 会为用户提供类似函数的await!宏,并且将来会提供类似后缀方法的.await!()宏。

@novacrazy

此外,虽然在这短短的时间内我已经越来越喜欢 postfix 关键字语法,但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 expr 。 这需要await?作为一个单位和特殊外壳; 这似乎没有根据,浪费了我们的复杂性预算,并且表明await expr存在严重问题。

更重要的是,Rust 代码,尤其是标准库,主要集中在点和方法调用语法的强大功能上。 当await是前缀时,它鼓励用户发明临时的 let 绑定而不是简单的链接方法。 这就是为什么?是postfix的原因,同样的道理, await也应该是postfix。

更糟糕的是await { ... } 。 如果按照rustfmt一致地格式化,则此语法将变成:

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

这不符合人体工程学,并且会显着增加功能的垂直长度。


相反,我认为等待,如? ,应该是后缀,因为它符合以方法链为中心的 Rust 生态系统。 已经提到了许多后缀语法; 我将通过其中一些:

  1. foo.await!() -- 这是后缀宏解决方案。 虽然我赞成后缀宏强烈的时候,我同意@cramertjhttps://github.com/rust-lang/rust/issues/50547#issuecomment -454225040,我们不应该这样做,除非我们也承诺后缀一般的宏。 我也认为以这种方式使用 postfix 宏给人一种相当不一流的感觉; 我们应该避免让语言结构使用宏语法。

  2. foo await -- 这还不错,它确实像一个后缀运算符( expr op ),但我觉得这种格式缺少一些东西(即感觉“空”); 相反, expr??直接附加到expr ; 这里没有空间。 这使得?看起来很吸引人。

  3. foo.await -- 这被批评为看起来像一个字段访问; 这是真的。 然而,我们应该记住await是一个关键字,因此它会被语法高亮显示。 如果您在 IDE 或 GitHub 上阅读 Rust 代码, await颜色或粗体将与foo 。 使用不同的关键字,我们可以证明这一点:

    let x = foo.match?;
    

    通常,字段也是名词,而await是动词。

    虽然有一个关于foo.await的初始嘲笑因素,但我认为应该认真考虑它作为一种视觉上吸引人的同时也是可读的语法。

    作为奖励,使用.await可为您提供点的强大功能以及点在 IDE 中通常具有foo.并且如果foo恰好是未来,则await将显示为第一选择。 这有助于人体工程学和开发人员的工作效率,因为许多开发人员已经训练成肌肉记忆。

    在所有可能的后缀语法中,尽管批评看起来像字段访问,但这仍然是我最喜欢的语法。

  4. foo# -- 这使用符号#等待foo 。 我认为考虑一个印记是个好主意,因为?也是一个印记,因为它使等待变得轻巧。 结合?看起来像foo#? - 看起来不错。 但是, #没有特定的理由。 相反,它只是一个仍然可用的印记。

  5. foo@ -- 另一个符号是@ 。 当与?结合使用时,我们得到foo@? 。 这个特定印记的一个理由是它看起来a -ish ( @wait )。

  6. foo! -- 最后是! 。 当与?结合使用时,我们得到foo!? 。 不幸的是,这有一定的 WTF 感觉。 但是, !看起来像强制值,适合“等待”。 有一个缺点是foo!()已经是一个合法的宏调用,所以等待和调用一个函数需要写成(foo)!() 。 使用foo!作为语法也会剥夺我们拥有关键字宏的机会(例如foo! expr )。

另一个符号是foo~ 。 波浪可以理解为“回声”或“需要时间”。 然而,它并未在 Rust 语言的任何地方使用。

波浪号~过去用于堆分配类型: https :

?可以重复使用吗? 还是魔法太多了? impl Try for T: Future会是什么样子?

@parasyte是的,我记得。 但还是很久没来了。

@jethrogb我无法看到impl Try直接工作, ?明确地returnTry来自当前函数的结果,而await需要yield

也许?可以特殊情况下在生成器的上下文中做其他事情,以便它可以yieldreturn取决于它所应用的表达式的类型,但我不确定这有多容易理解。 又怎么会是互动Future<Output=Result<...>> ,你必须let foo = bar()??;来都做“的await”,然后拿到Ok的变型的Result (或者生成器中的?是否基于可以yieldreturn或解析为单个应用程序的值的三态特征?

最后一个括号里的评论实际上让我觉得它是可行的,点击查看快速草图
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()),
    }
}

不幸的是,我不知道如何在这样的方案中处理唤醒上下文变量,也许如果?是特殊的async而不是生成器,但如果它是特殊的-在这里,如果它可用于其他生成器用例,那就太好了。

我对重新使用?和@jethrogb 有同样的想法。

@尼莫157

我无法看到impl Try直接工作, ?显式return是当前函数的Try结果,而 await 需要yield

也许我错过了?Try特性的一些细节,但是哪里/为什么是明确的? 无论如何,异步闭包中的return是否与yield本质上相同,只是不同的状态转换?

也许?可以特殊情况下在生成器的上下文中做其他事情,以便它可以yieldreturn取决于它所应用的表达式的类型,但我不确定这有多容易理解。

我不明白为什么这应该令人困惑。 如果您将?视为“继续或发散”,那么这似乎很自然,恕我直言。 当然,更改Try特征以对关联的返回类型使用不同的名称会有所帮助。

还有如何与Future<Output=Result<...>>交互,您是否必须let foo = bar()??

如果您想等待结果,然后在出现错误结果时提前退出,那么这就是逻辑表达式,是的。 我认为根本不需要特殊的三态TryGen

不幸的是,我不知道如何在这样的方案中处理唤醒上下文变量,也许如果? 是异步而不是生成器的特殊情况,但如果它在这里是特殊情况,如果它可用于其他生成器用例,那就太好了。

我不明白这部分。 你能详细说明一下吗?

@jethrogb @rolandsteiner一个结构体可以同时实现TryFuture 。 在这种情况下, ?解开哪一个?

@jethrogb @rolandsteiner一个结构体可以同时实现 Try 和 Future。 在这种情况下,应该选择哪一个? 解开?

不,它不能,因为笼统的 impl Try for T: Future。

为什么没有人谈论显式构造和隐式等待提案? 它等同于sync io,只是它阻塞的是任务而不是线程。 我什至会说阻塞线程比阻塞任务更具侵入性,那么为什么线程阻塞io没有特殊的“等待”语法?

但这只是自行车着色,我认为我们至少现在应该满足于简单的宏语法await!(my_future)

但这只是自行车着色,我认为我们至少现在应该满足于简单的宏语法await!(my_future)

不,这不仅仅是“单车脱落”,好像那是平庸和微不足道的事情。 await是写前缀还是后缀从根本上影响异步代码的编写方式。 方法链以及它的可组合性。 稳定await!(future)还需要放弃await作为关键字,这使得未来无法将await用作关键字。 “至少现在”表明我们可以稍后找到更好的语法,而忽略由此带来的技术债务。 我反对故意为以后要替换的语法引入债务。

稳定 await!(future) 还需要放弃作为关键字的 await,这使得将来无法将 await 作为关键字使用。

我们可以在下一个纪元中将其设为关键字,需要宏的原始 ident 语法,就像我们对try所做的那样。

@rolandsteiner

无论如何,异步闭包中的return是否与yield本质上相同,只是不同的状态转换?

yield不存在于异步闭包中,它是在从async / await语法降低到 generators/ yield期间引入的操作。 在当前的生成器语法中yieldreturn完全不同,如果?扩展在生成器转换之前完成,那么我不知道它如何知道何时插入returnyield

如果您想等待结果并且 nalso 在错误结果中提前退出,那么这将是逻辑表达式,是的。

这可能是合乎逻辑的,但对我来说似乎是一个缺点,在您编写异步函数的很多(大多数?)情况下,将填充双??以处理 IO 错误。

不幸的是,我不知道如何处理唤醒上下文变量......

我不明白这部分。 你能详细说明一下吗?

异步转换在生成的Future::poll函数中接收一个唤醒变量,然后需要将其传递到转换后的 await 操作中。 目前,这是使用std提供的 TLS 变量处理的,这两个变量都转换引用,如果?被处理为重新生成点_在生成器级别_,则异步转换在某种程度上失败插入此变量引用。

两个月前,我写了一篇关于await语法的博客文章,概述了我的偏好。 但是,它基本上假定了前缀语法,并且只是从这个角度考虑了优先级问题。 以下是一些额外的想法:

  • 我的总体看法是,Rust 真的已经超出了它的陌生预算。 让来自 JavaScript、Python 或 C# 的人尽可能熟悉表面级 async/await 语法将是理想的选择。 从这个角度来看,理想的做法是仅以较小的方式偏离规范。 后缀语法因它们的分歧程度而异(例如, foo await的分歧比诸如foo@类的符号要小),但它们都比前缀 await 更分歧。
  • 我也更喜欢稳定不使用!的语法。 每个处理 async/await 的用户都会想知道为什么 await 是一个宏而不是一个正常的控制流结构,我相信这里的故事本质上是“我们无法找出一个好的语法,所以我们只是决定让它看起来像一个宏。” 这不是一个令人信服的答案。 我不认为!和控制流之间的关联真的足以证明这种语法是正确的:我相信!有非常明确的意思是宏扩展,这不是。
  • 我有点怀疑 postfix await 一般的好处(不完全是,只是有点)。 我觉得平衡与?有点不同,因为等待是一个更昂贵的操作(你在循环中产生直到它准备好,而不是仅仅分支和返回一次)。 我有点怀疑在一个表达式中等待两三次的代码; 在我看来,这些应该被拉出到它们自己的 let 绑定中似乎很好。 所以try!?权衡在这里对我没有那么强烈。 而且,我愿意接受人们认为不应该将其放入 let 并且作为方法链更清晰的代码示例。

也就是说, foo await是迄今为止我见过的最可行的后缀语法:

  • 对于后缀语法来说,它比较熟悉。 所有你需要学习的是,在 Rust 中,await 在表达式之后而不是之前,而不是明显不同的语法。
  • 它清楚地解决了所有这一切所涉及的优先级问题。
  • 由于我之前提到的原因,它在方法链上不能很好地工作这一事实对我来说几乎是一个优势,而不是一个劣势。 如果我们有一些语法规则阻止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;
  • 让来自 JavaScript、Python 或 C# 的人尽可能熟悉表面级 async/await 语法将是理想的选择。

try { .. }的情况下,我们考虑了对其他语言的熟悉程度。 然而,从与 Rust 内部一致性的 POV 来看,这也是正确的设计。 因此,尽管尊重其他语言,Rust 的内部一致性似乎更重要,而且我认为前缀语法在优先级或 API 的结构方面都不适合 Rust。

  • 我也更喜欢稳定不使用!的语法。 每个处理 async/await 的用户都会想知道为什么 await 是一个宏而不是一个正常的控制流结构,我相信这里的故事本质上是“我们无法找出一个好的语法,所以我们只是决定让它看起来像一个宏。” 这不是一个令人信服的答案。

我同意这种观点, .await!()看起来不够一流。

  • 我有点怀疑 postfix await 一般的好处(不完全是,只是 _sort of_)。 我觉得平衡与?有点不同,因为等待是一个更昂贵的操作(你在循环中产生直到它准备好,而不是仅仅分支和返回一次)。

我不明白将东西提取到let绑定中的代价有多大。 方法链有时也很昂贵。 let绑定的好处是 a) 给足够大的块一个名称,以提高可读性,b) 能够多次引用相同的计算值(例如通过&x或当类型可复制时)。

我有点怀疑在一个表达式中等待两三次的代码; 在我看来,这些应该被拉出到它们自己的 let 绑定中似乎很好。

如果您觉得应该将它们提取到自己的let绑定中,您仍然可以使用 postfix await做出选择:

let temporary = some_computation() await?;

对于那些不同意并喜欢方法链的人,后缀await提供了选择的能力。 我也认为 postfix 更好地遵循从左到右的阅读和数据流顺序,所以即使你提取到let绑定,我仍然更喜欢 postfix。

我也不认为你需要等待两三次后缀await才有用。 考虑例如(这是rustfmt ):

    let foo = alpha()
        .beta
        .some_other_stuff()
        .await?
        .even_more_stuff()
        .stuff_and_stuff();

而且,我愿意接受人们认为不应该将其放入 let 并且作为方法链更清晰的代码示例。

当提取到let绑定和let binding = await!(...)?;时,我阅读的大多数紫红色代码都感觉不自然。

  • 对于后缀语法来说,它比较熟悉。 所有你需要学习的是,在 Rust 中,await 在表达式之后而不是之前,而不是明显不同的语法。

我对foo.await偏爱主要是因为您可以获得很好的自动完成功能和点的力量。 感觉也没有太大的不同。 编写foo.await.method()还可以更清楚地说明.method()应用于foo.await 。 所以它解决了这个问题。

  • 它清楚地解决了所有这一切所涉及的优先级问题。

不,这不仅仅是关于优先级。 方法链同样重要。

  • 由于我之前提到的原因,它在方法链上不能很好地工作这一事实对我来说几乎是一个优势,而不是一个劣势。

我不确定为什么它不能很好地与方法链接一起使用。

如果我们有一些语法规则阻止foo await.method()仅仅因为我真的觉得这个方法被(荒谬地)应用于await ,而不是foo (而有趣的是我不觉得foo await? )。

而如果我们引入了有意设计的剪纸并防止使用后缀await语法进行方法链接,我将不会被迫使用foo await

承认每个选项都有缺点,而且其中一个最终应该被选中......让我困扰foo.await一件事是,即使我们假设它不会被误认为是结构字段,它看起来仍然最有副作用的操作之一(它既执行在 Future 中构建的 I/O 操作,具有控制流效果)。 所以当我读到foo.await.method() ,我的大脑告诉我跳过.await因为它相对无趣,我必须用注意力和努力来手动覆盖这种本能。

它仍然_看起来像_访问结构字段。

@glaebhoerl.await颜色与其他东西不同。

字段访问的含义是没有发生特别有影响的事情——它是 Rust 中最不有效的操作之一。 同时等待是非常有影响力的,是最有副作用的操作之一(它既执行在 Future 中构建的 I/O 操作,又具有控制流效果)。

我非常同意这一点。 await是像breakreturn一样的控制流操作,应该是明确的。 建议的后缀表示法感觉不自然,就像 Python 的if :将if c { e1 } else { e2 }e1 if c else e2 。 无论任何语法高亮显示,看到末尾的运算符都会让您进行双重检查。

我也看不出e.await是如何比await!(e)await e更符合 Rust 语法的。 没有其他后缀关键字,并且由于其中一个想法是在解析器中对其进行特殊处理,我认为这不是一致性的证明。

还有提到的熟悉度问题@withoutboats 。 如果它有一些奇妙的好处,我们可以选择奇怪而奇妙的语法。 但是,后缀await有吗?

语法高亮对它的外观和大脑处理事物的方式没有/不足的影响吗?

(好问题,我敢肯定它会产生一些影响,但如果不实际尝试,很难猜测有多大影响(并且替换一个不同的关键字只能到此为止)。虽然我们在这个主题上......很长一段时间之前我提到我认为语法突出显示应该突出显示所有具有控制流效果的运算符( returnbreakcontinue? ... 现在await ) 以某种特殊的特别独特的颜色,但我不负责任何语法突出显示,也不知道是否有人真的这样做了。)

我非常同意这一点。 await是像breakreturn一样的控制流操作,应该是明确的。

我们同意。 符号foo.await , foo await , foo# , ...是明确的。 没有隐式等待完成。

我也看不出e.await是如何比await!(e)await e更符合 Rust 语法的。

语法e.await本身与 Rust 语法不一致,但 postfix 通常更适合?以及 Rust API 的结构方式(方法优于自由函数)。

await e?语法,如果关联为(await e)?则与breakreturn关联方式完全不一致。 await!(e)也不一致,因为我们没有控制流的宏,它也有与其他前缀方法相同的问题。

没有其他后缀关键字,并且由于其中一个想法是在解析器中对其进行特殊处理,我认为这不是一致性的证明。

我认为您实际上根本不需要为.await更改 libsyntax,因为它应该已经作为现场操作处理了。 逻辑宁愿在解析或 HIR 中处理,您将其转换为特殊构造。

如果它有一些奇妙的好处,我们可以选择奇怪而奇妙的语法。 不过,后缀await有吗?

如前所述,我认为这是由于方法链和 Rust 对方法调用的偏好。

我认为您实际上根本不需要为 .await 更改 libsyntax,因为它应该已经作为现场操作处理了。

这很好玩。
所以这个想法是重用self / super /... 的方法,但用于字段而不是路径段。

这有效地使await成为路径段关键字(因为它通过解析),因此您可能希望禁止它的

#[derive(Default)]
struct S {
    r#await: u8
}

fn main() {
    let s = ;
    let z = S::default().await; //  Hmmm...
}

没有隐式等待完成。

这个想法在这个线程上出现了几次(“隐式等待”提案)。

我们没有控制流的宏

try! (很好地select! 。 请注意, awaitreturn “更强”,因此期望它在代码中比?return更明显并不是没有道理的。

我认为这是由于方法链和 Rust 对方法调用的偏好。

它还对前缀控制流操作符有(更明显的)偏好。

等待 e? 语法,如果关联为 (await e)? 与如何中断和返回关联完全不一致。

我更喜欢await!(e)?await { e }?甚至{ await e }? - 我认为我没有看到后者讨论过,我不确定它是否有效。


我承认可能有从左到右的偏见。 _笔记_

每次看到这个问题,我对这个问题的看法似乎都会改变,就好像在为自己辩护。 部分原因是我太习惯于编写自己的期货和状态机。 带有poll自定义期货是完全正常的。

或许这应该换一种方式思考。

对我来说,Rust 中的零成本抽象是指两件事:运行时的零成本,更重要的是精神上的零成本。

我可以很容易地推理出 Rust 中的大多数抽象,包括期货,因为它们只是状态机。

为此,应该存在一个简单的解决方案,为用户引入尽可能少的魔法。 印记尤其是一个坏主意,因为它们感觉不必要地神奇。 这包括.await魔术字段。

也许最好的解决方案是最简单的一个,原始的await!宏。

因此,尽管尊重其他语言,Rust 的内部一致性似乎更重要,而且我认为前缀语法在优先级或 API 的结构方面都不适合 Rust。

我不明白怎么...? await(foo)? / await { foo }?在运算符优先级和 API 在 Rust 中的结构方面似乎完全没问题 - 它的缺点是括号和(取决于你的观点)链接的冗长,而不是打破先例或被令人困惑。

try! (很好地select!

我认为这里的操作词已被弃用。 在 Rust 2018 上使用try!(...)是一个硬错误。现在它是一个硬错误,因为我们引入了更好的、一流的和后缀语法。

请注意, awaitreturn “更强”,因此期望它在代码中比?return更明显并不是没有道理的。

?操作符同样可以产生副作用(通过其他实现而不是Result )并执行控制流,因此它也非常“强大”。 在讨论时, ?被指“隐瞒回报” ,容易被忽视。 我认为那个预测完全没有实现。 情况再。 await似乎和我很相似。

它还对前缀控制流操作符有(更明显的)偏好。

这些前缀控制流操作符的类型为! 。 同时,另一个控制流运算符?接受上下文impl Try<Ok = T, ...>并为您提供T是后缀。

我不明白怎么...? await(foo)? / await { foo }?在运算符优先级和 API 在 Rust 中的结构方面似乎完全没问题-

如果前者需要括号而后者不需要括号,则await(foo)语法与await foo 。 前者是前所未有的,后者有优先权问题。 ?正如我们在此处、在boat 的博客文章和 Discord 上所讨论的那样。 await { foo }语法由于其他原因存在问题(参见 https://github.com/rust-lang/rust/issues/50547#issuecomment-454313611)。

它的缺点是括号和(取决于您的观点)链接的冗长,而不是打破先例或令人困惑。

这就是我所说的“API 是结构化的”的意思。 我认为方法和方法链在 Rust 中是常见且惯用的。 前缀和块语法与那些和?组合很差。

持这种观点的我可能属于少数,如果是这样,请忽略我:

将前缀与后缀的讨论转移到内部线程,然后简单地返回这里是否公平? 这样我们就可以保持跟踪问题来跟踪功能的状态

@seanmonstar是的,我强烈同意限制对跟踪问题的讨论并且这里开了一个新问题供我们讨论。

对所有人重要:进一步的await语法讨论应该在这里

暂时锁定一天,以确保未来关于await语法的讨论发生在适当的问题上。

在 2019 年 1 月 15 日星期二上午 07:10:32 -0800,Pauan 写道:

旁注:总是可以使用括号来使优先级更清晰:

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

这违背了 postfix await 的主要好处:“只要保持
写/读”。后缀等待,就像后缀? ,允许控制流
继续从左向右移动:

foo().await!()?.bar().await!()

如果await!是前缀,或者当try!是前缀时返回,或者如果您有
括号,然后你必须跳回到左边
书写或阅读时的表达。

编辑:我正在通过电子邮件从头到尾阅读评论,直到发送此邮件后才看到“将对话移至另一个问题”评论。

异步等待状态报告:

http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/


我想发布有关 async-await 状态的快速更新
努力。 简短的版本是我们在主场
某种程度的稳定,但仍有一些重要的
要克服的问题。

宣布成立实施工作组

作为此次推动的一部分,我很高兴地宣布我们已经成立了一个
async-await 实现工作组。 这个工作组
是整个 async-await 工作的一部分,但专注于
实现,并且是编译器团队的一部分。 如果你愿意
帮助在终点线上进行异步等待,我们有一个问题列表
我们肯定需要帮助(请继续阅读)。

如果您有兴趣参加,我们有“办公时间”定于星期二(请参阅 [编译器团队日历]) --如果您
可以出现在[Zulip]上,那将是理想的! (但如果没有,只需弹出任何
时间。)

...

std::future::Future会稳定? 它是否必须等待异步等待? 我认为这是一个非常好的设计,并希望开始将代码移植到它。 (是否有垫片可以在稳定中使用它?)

@ry查看它的新跟踪问题: https :

async/await 的另一个编译器问题: https :

另请注意,可以检查顶部帖子中的https://github.com/rust-lang-nursery/futures-rs/issues/1199 ,因为它现在已修复。

看起来 HRLB 和异步闭包存在问题: https :

是的,异步闭包有很多问题,不应该包括在最初的稳定阶段。 当前的行为可以用闭包 + 异步块来模拟,将来我很想看到一个允许从返回的未来引用闭包的 upvar 的版本。

我刚刚注意到,目前await!(fut)要求futUnpinhttps :

这是预期的吗? 它似乎不在 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);

因为BoxUnpin并且允许从中移动值,您可以在一个堆位置轮询一次未来,然后将未来移出框并将其放入新的堆位置并轮询再说一遍。

await 应该可能是特殊情况以允许Box<dyn Future>因为它消耗了未来

也许IntoFuture特质应该为await!复活? Box<dyn Future>可以通过转换为Pin<Box<dyn Future>>

这是我的下一个 async/await 错误:它看起来像在async fn的返回类型中使用关联类型到类型参数中断推理: https :

除了可能将#60414 添加到 top-post 的列表中(不知道它是否还在使用——也许指向 github 标签会更好?),我认为“rust-lang/可以勾选 rfcs#2418”,因为 IIRC 的Future特性最近已经稳定。

我刚从 Reddit 上的一个帖子回来,我必须说我根本不喜欢 postfix 语法。 而且似乎大多数 Reddit 也不喜欢它。

我宁愿写

let x = (await future)?

而不是接受那种奇怪的语法。

至于链接,我可以重构我的代码以避免超过 1 await

此外,未来的 JavaScript 可以做到这一点(智能管道提案):

const x = promise
  |> await #
  |> x => x.foo
  |> await #
  |> x => x.bar

如果前缀await被实现,这并不意味着await不能被链接。

@KSXGitHub这真的不是讨论的地方,但这里概述了逻辑,并且有很多人已经考虑了好几个月的很好的理由https://boats.gitlab.io/blog/post /等待决定/

@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 据我了解,最终语法已经达成一致,也许是时候将其标记为“完成”了? :脸红:

是打算在 7 月 4 日的下一次测试版削减前及时稳定下来,还是阻止错误需要另一个周期来解决? A-async-await 标签下有很多未解决的问题,但我不确定其中有多少是关键的。

啊哈,不管这个,我刚刚发现了AsyncAwait-Blocking标签。

你好呀! 我们什么时候需要期待这个功能的稳定版本? 我如何在夜间构建中使用它?

@MehrdadKhnzd https://github.com/rust-lang/rust/issues/62149包含有关目标发布日期等的信息

是否有计划为async fn生成的期货自动实施Unpin async fn

具体来说,我想知道 Unpin 是否由于生成的 Future 代码本身而无法自动使用,或者因为我们是否可以使用引用作为参数

@DoumanAsh我想如果异步 fn 在屈服点从来没有任何活动的自引用,那么生成的 Future 可以想象地实现 Unpin,也许?

我认为这需要伴随一些非常有用的错误消息,说“不是Unpin因为 _this_ 借用”+ 暗示“或者你可以框住这个未来”

#63209 中的稳定 PR 指出“所有阻止程序现在都已关闭。” 并于 8 月 20 日每晚登陆,因此本周晚些时候将进行测试版削减。 似乎值得注意的是,自 8 月 20 日以来,已经提交了一些新的阻塞问题(由 AsyncAwait-Blocking 标记跟踪)。 其中两个(#63710、#64130)看起来不错,实际上不会妨碍稳定,但是还有其他三个问题(#64391、#64433、#64477)似乎值得讨论。 后三个问题是相关的,所有这些问题都是由 PR #64292 引起的,它本身是为了解决 AsyncAwait-Blocking 问题 #63832。 PR,#64584,已经登陆试图解决大部分问题,但目前这三个问题仍然悬而未决。

一线希望是三个严重的开放阻塞程序似乎与应该编译但当前未编译的代码有关。 从这个意义上说,它将向后兼容稍后的土地修复,而不会妨碍 async/await 的 Beta 化和最终稳定。 但是,我想知道 lang 团队中的任何人是否认为这里的任何事情都足以表明 async/await 应该每晚烘烤另一个周期(这听起来令人反感,这快速发布计划的重点毕竟)。

@bstrie我们只是重用“AsyncAwait-Blocking”,因为没有更好的标签来将它们标记为“高优先级”,它们实际上并没有阻塞。 我们应该尽快修改标签系统以减少混淆,cc @nikomatsakis。

......不好......我们在预期的 1.38 中错过了 async-await。 不得不等待 1.39,只是因为一些不重要的“问题”......

@earthengine我认为这不是对情况的公平评估。 出现的问题都值得认真对待。 仅让人们在实践中尝试使用它时遇到这些问题时,登陆 async await 是不好的:)

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

alexcrichton picture alexcrichton  ·  240评论

withoutboats picture withoutboats  ·  211评论

nikomatsakis picture nikomatsakis  ·  412评论

nikomatsakis picture nikomatsakis  ·  340评论

withoutboats picture withoutboats  ·  202评论