【问题标题】:Why is the code before DART async modifies the method await synchronized?为什么DART async 修改方法await 之前的代码是同步的?
【发布时间】:2021-06-08 04:22:51
【问题描述】:

这是使用 Future 的代码

void main(List<String> arguments) {
  testFuture();
  print('main');
}

Future testFuture() {
  return Future(() {
    for (var i = 0; i < 1000000000; i++) {}
    print('futureDone');
  });
}

输出如下

main
futureDone

当我想使用 async 来简化我的代码时,代码如下所示

Future testFuture() async{
  for (var i = 0; i < 1000000000; i++) {}
  print('futureDone');
}

输出如下

futureDone
main

一开始我很迷茫,不知道为什么他们的输出不一样,找了很多资料,直到在https://dart.dev/codelabs/async-await#execution-flow-with-async-and-await看到这些字。

async 函数同步运行,直到第一个 await 关键字。这意味着在异步函数体内,第一个 await 关键字之前的所有同步代码都会立即执行。

我不知道为什么,所以考虑到这一点,如果我想使用异步,我需要编写这样的代码。

Future testFuture() async{
  await Future.delayed(Duration(seconds: 0));
  for (var i = 0; i < 1000000000; i++) {}
  print('futureDone');
}

输出如下

main
futureDone

通过阅读旧版Dart中的StackFlow problem,发现旧版没有 “一个 async 函数会同步运行,直到第一个 await 关键字。这意味着在一个 async 函数体内,第一个 await 关键字之前的所有同步代码都会立即执行。”,DART 为什么会做出这样的改变?还是我的使用方式有问题?

【问题讨论】:

  • 更改上下文总是要付出代价的。您希望尽可能多地执行而不是仅仅将事件放在我们将立即执行的事件队列中,这是有道理的。例如。 async 方法中的第一个代码很可能会调用一些我们想要等待结果的代码。也许,有可能我们并不总是以await 结束某事(例如,如果我们实现某种缓存)。

标签: dart async-await


【解决方案1】:

LRN 答案代码示例

这是旧版代码

void main(List<String> args) {
  maybeDoFoo().then((value) => print(value));
  maybeDoFoo().then((value) => print(value));
}
bool mayDoFoo = true;
Future maybeDoFoo() async{
  await Future.delayed(Duration(seconds: 0));
  if (mayDoFoo) return _doFoo();
  return("Can't do foo!");
}
Future _doFoo() async{
  await Future.delayed(Duration(seconds: 0));
  mayDoFoo = false;
  await Future.delayed(Duration(seconds: 5));
  var result = 1;
  mayDoFoo = true;
  return result;
}

打印:

1

1

现在

void main(List<String> args) {

  maybeDoFoo().then((value) => print(value));
  maybeDoFoo().then((value) => print(value));
}
bool mayDoFoo = true;
Future maybeDoFoo() async{
  // await Future.delayed(Duration(seconds: 0));
  if (mayDoFoo) return _doFoo();
  return("Can't do foo!");
}
Future _doFoo() async{
  // await Future.delayed(Duration(seconds: 0));
  mayDoFoo = false;
  await Future.delayed(Duration(seconds: 5));
  var result = 1;
  mayDoFoo = true;
  return result;
}

打印:

做不到!

1

【讨论】:

    【解决方案2】:

    之所以进行更改,是因为延迟异步函数启动的旧行为妨碍了人们做他们想做的事情。

    假设您想要获取流的前十个元素。你可以这样写:

    Future<List<T>> getTen<T>(Stream<T> stream) async {
      var list = <T>[];
      await for (var event in stream) {
        list.add(event);
        if (list.length == 10) break;
      }
      return list;
    }
    

    看起来非常好。但是,如果流是广播流,并且async 函数延迟了代码的第一部分,那么您不会立即收听流,并且可能会错过一些事件。

    另一个问题是诸如重入检查之类的事情。如果你想防止调用一个函数两次,你可以这样做:

    Result maybeDoFoo() {
      if (_mayDoFoo) return _doFoo();
      throw "Can't do foo!";
    }
    Result _doFoo() {
      mayDoFoo = false;
      var result = doTheActualFoo();
      mayDoFoo = true;
      return result;
    }
    

    这里的代码保护你不要在它已经执行时调用doFoo。 然后你尝试使函数异步,可能是因为doTheActualFoo 需要依赖已经异步的东西,然后你写:

    Future<Result> maybeDoFoo() async {
      if (_mayDoFoo) return _doFoo();
      throw "Can't do foo!";
    }
    Future<Result> _doFoo() async {
      mayDoFoo = false;
      var result = doTheActualAsyncFoo();
      mayDoFoo = true;
      return result;
    }
    

    如果异步主体的初始部分被延迟,那么两者之间存在异步间隙 if (mayDoFoo)mayDoFoo = false,这意味着您可以调用doFoo 两次,都将mayDoFoo 视为true,然后稍晚它们都进入_doFoo 函数。这完全破坏了这种模式的安全性。 这样的代码是在野外发现的。

    async 函数的行为已更改为立即运行以避免此类错误。它的原始行为太不可预测,并且在同步代码中运行良好的代码模式在直接移植到异步时会变得微妙地脆弱。随着这一变化,延迟只发生在awaits,而不是其他任何地方,这使得实际发生的事情更容易预测。 (嗯,差不多,因为 async* 函数在其流上调用 listen 时仍然延迟,因此它们可能会丢失广播流事件。)

    【讨论】:

    • 感谢您的回答。我花了一些时间才明白你的意思。
    • 首先,我了解到不仅'await'之前的代码是同步的,而且'await'所在行的代码也是同步的。然后我了解了您的第一个 Stream 示例。
    • 然后我花了一些时间修改您的第二个示例以使其运行,并且我看到了它如何防止双重调用。
    • 这确实是一个必要的改变,但是对于我这样的新手来说,我觉得很难理解。
    猜你喜欢
    • 2015-07-04
    • 1970-01-01
    • 2019-04-14
    • 2017-10-26
    • 2017-05-02
    • 1970-01-01
    • 2020-04-20
    • 2019-06-06
    相关资源
    最近更新 更多