【问题标题】:How do async/await/then really work in Dart?async/await/then 如何在 Dart 中真正起作用?
【发布时间】:2022-01-19 13:08:51
【问题描述】:

这可能是一个反复出现的问题,但我发现 冲突 答案,我现在对其中哪个是正确的答案感到困惑。我以为我理解了这个概念,然后我开始阅读所有这些答案并完全感到困惑,所以我正在寻找一个确定简单我可以轻松解决的问题的答案理解。

根据this answerthis articleawait 应该中断代码执行,实际上等待未来完成,然后继续执行其余部分的代码顺序。它还表明这可能会阻塞主线程,这仅在这种情况下是合乎逻辑的。

另一方面,thisthisthis video from the flutter team 建议 await 不会阻止其余代码执行,它只是注册回调的语法糖在未来结束时执行,这与then 所做的相同。

现在,我尝试编写一个小程序来了解其中哪些是正确的,似乎第一种方法是要走的路:

import 'dart:async';

// prints: 
// 1000+
// 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1)).then((_){print(watch.elapsedMilliseconds);});
  
  print(watch.elapsedMilliseconds); 
  
}

反对:

import 'dart:async';

// prints:
// 0
// 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  Future.delayed(Duration(seconds:1)).then((_){print(watch.elapsedMilliseconds);});
  
  print(watch.elapsedMilliseconds);
  
}

所以我只想知道为什么flutter团队和一些人建议await不阻塞代码执行以及这个概念是如何真正起作用的。

【问题讨论】:

  • 我认为您将线程与事件循环混淆了。如您所知,只有在线程上,但由于事件循环,该线程似乎可以一次执行很多事情。它正在执行的任何事情都可以暂停并(a)等待事情,因为它们放弃了线程。这允许其他事物在其事件(定时器、网络、I/O 等)完成时交错。有关事件循环的更多信息,请参阅:medium.com/dartlang/…
  • 阻止线程的唯一方法是执行一些计算绑定(将 pi 计算到 10000 个位置、计算数字签名、解码大图像等)。这就是 Dart 有孤立物的原因。通过这种方式,您可以创建第二个线程,该线程可以消耗所有核心来执行计算密集型任务,同时让您的原始主隔离能够对事件做出反应。

标签: multithreading flutter dart async-await


【解决方案1】:

我认为对阻塞存在一些误解。当您查看您的第一个示例时 - await 将仅阻止执行 您的函数中的其余代码。您的应用程序的其余部分仍然可以正常工作。

您需要了解一件事:async/await 语法只是 .then(callback) 语法的语法糖。它们都实现了相同的目标,只有 async/await 更容易阅读、调试和理解。如您所见-在您的两个示例中,您都得到了相同的结果。您的问题是:您更喜欢哪种语法?

为了澄清 - 让我们假设您要引入几个 1 秒的等待事件,并在每个事件之后写一条消息。

您的第一个示例将如下所示:

import 'dart:async';

// prints 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1));
  print(watch.elapsedMilliseconds); 

  await Future.delayed(Duration(seconds:1));
  print(watch.elapsedMilliseconds); 

  await Future.delayed(Duration(seconds:1));
  print(watch.elapsedMilliseconds); 

  
}

请注意阅读和理解代码是多么容易。

现在,把第二个例子改成同样的事情:

import 'dart:async';

void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  Future.delayed(Duration(seconds:1)).then((_){
    print(watch.elapsedMilliseconds);
    Future.delayed(Duration(seconds:1)).then((_){
        print(watch.elapsedMilliseconds);
        Future.delayed(Duration(seconds:1)).then((_){
             print(watch.elapsedMilliseconds);
        });
    });
  });
}

它们都会达到相同的效果 - 但第二个例子会让你的眼睛受伤。

您需要考虑的一个更有趣的场景是 - 如果您希望同时发生几件事情怎么办?这并不罕见 - 如果您需要从 3 个不同的服务器获取 3 个图像,您不会按顺序获取它们。您可能希望同时触发所有 3 个请求,并等待它们全部完成。

使用 async/await 这很容易:

import 'dart:async';

// prints 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  var f1 = Future.delayed(Duration(seconds:1));
  var f2 = Future.delayed(Duration(seconds:2));
  var f3 = Future.delayed(Duration(seconds:3));

  await Future.wait([f1, f2, f3]);

  print(watch.elapsedMilliseconds); 

  
}

请注意,由于我们没有在每个 Future.delayed 前面放置 await - 这意味着我们将启动延迟的未来,但我们不会等待它完成。

你会看到整个功能只需要3秒就可以完成;因为所有 3 个计时器同时运行。 Future.wait 将等待期货列表完成。

现在 - 很明显,在大多数情况下您并不真正需要 .then() 语法,但我认为它仍然适用于更复杂的场景。

例如:您需要从 3 个服务器获取 3 个图像。这些服务器中的每一个都有一个备份服务器;如果第一台服务器返回 null 作为结果 - 您需要从备份服务器获取资源。 另外:如果Backup server 1或Backup server 2返回null,则需要调用server 4获取单张图片。

您甚至可以绘制一个小图来描述这一点。现在这就是 .then() 语法派上用场的地方——我们仍然会将它与 async/await 结合起来。我想一旦你完全理解了这个例子——你就会非常理解 async/await 和 .then()。走吧:

import 'dart:async';
import 'dart:math';

Future<int?> getImage(String server) async {
  var rng = Random();
  
  print("Downloading from $server");
  
  // we'll add random delay to simulate network
  await Future.delayed(Duration(seconds: rng.nextInt(5)));
  
  print("$server is done");
  
  // high chance of returning null
  if (rng.nextInt(10)<7) return null;
  return 1;
}

// prints 1000+
void main() async {
  
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  // get the image from server 1
  var f1 = getImage("Server 1").then((data) async { 
     return data ?? await getImage("Server 1 backup");
  });
  
  var f2 = getImage("Server 2").then((data) async { 
     return data ?? await getImage("Server 2 backup");
  });

  var f4=Future.wait([f1, f2]).then((data) async {
    if (data[0]==null || data[1]==null) {
       return [await getImage("Server 4")];
    } else {
       return data;
    }
  });
  
  var f3 = getImage("Server 3").then((data) async { 
     return data ?? await getImage("Server 3 backup");
  });

  await Future.wait([f3, f4]);

  print("elapsed ${watch.elapsedMilliseconds} ms"); 
  
}

这里有一个新的东西:.then() 将返回一个未来对象 - 你仍然可以使用 await 关键字等待。告诉过你是一样的......

如果没有 .then() 语法,您将需要再创建一个异步函数来处理此问题,这会使您的代码有点复杂且更难以阅读。使用 .then() 语法,代码更易于管理。再看一遍 - .then() 和 async/await 实际上是一回事......

当事情是线性的时,标准的 async/await 会有所帮助(比如我展示的多个 Future.delayed 示例)。但是,当您遇到可以通过多个并行运行的分支的 Graph 来描述的复杂场景时,.then() 将派上用场。

编辑 - Dart 是单线程

关于 Dart 是单线程的,这样想:你的代码在 Dart 引擎(或 Dart VM)中运行,而这段代码确实是单线程的。但是对外部世界的任何调用都将并行运行(调用远程服务器,甚至调用本地硬盘驱动器,调用同一主机上的其他进程,如 OS - 是的,甚至像我的示例中那样调用计时器)。

就像我上面的例子:我调用了 3 个远程服务器来获取一些东西,我链接了 3 个不同的回调,每个调用 1 个。而“外界事物”——调用服务器——实际上是同时发生的。 Dart 的单线程只是保证在任何给定时间点我的代码中只有一行代码会被执行。

如果您有 Java 背景,您就会知道在 Java 中同步多个线程是多么困难:这也是代码经常中断的地方。在 Dart 中,您无需担心这一点。真正的性能优化在于,在 Dart VM 之外发生的任何事情实际上都是并行运行的 - Dart 会为您处理。

现在这是如何工作的:事件循环。这是一个小型 dart 引擎,可以跟踪所有远程服务器调用,并在准备好时回调您的 - 好吧,回调过程。事件循环是负责您的代码同时处理一个请求的一种......

【讨论】:

  • 感谢您非常彻底的回答。但是,我不同意您的观点,即我的两个示例都产生了相同的结果。我在第一个示例中编辑了代码并将 .then() 链接到未来,这使程序打印 1000+ 两次而不是 0 和 1000+。不过,你的最后一个例子很好。我几乎可以理解,我们可以将 .then() 用于后备场景,或者在 then 子句中使用第一个未来的结果来做其他事情。我唯一的问题是:如果 dart 是单线程的,如何在我的函数之外继续执行 - 正如你所提到的那样?
  • 您是对的 - 在您的两种情况下,确切的输出都不相同。问题是:你想从逻辑上实现什么?如果您的目标是让最后一个打印语句打印程序的总持续时间 - 那么第一种方法是正确的。或者以不同的方式理解它:如果存在依赖关系,那么第二次打印只能在第一次打印完成后运行 - 那么第一种方法是正确的。如果没有依赖关系 - 那么逻辑上两者都很好;但是在第二种方法中,您更早地执行了第二个打印命令 - 优化了整个过程。
  • 我正忙着写答案,忘记回复您的评论了。您的解释和回答帮助我了解了我目前对这一切如何运作的理解。确实,这一切都基于您最终想要实现的目标以及您的目标是什么。您必须真正了解您希望代码如何在程序中流动,并很好地掌握事件循环的工作原理。只有这样,您才能实现您正在寻找的东西。感谢您的精彩回答!
【解决方案2】:

Andrija 的回答在技术上是正确的。但是,在我能够理解它的真正工作原理之前,我仍然需要考虑很多,这就是为什么我会尝试为任何可能有相同问题的人简化事情。

假设你有一个飞镖程序;显然是main()。我们的程序调用了两个函数; foo()bar()

函数foo() 做了一些异步工作,例如网络调用:

Future<void> foo() async{
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1));
  
  print(watch.elapsedMilliseconds);
}

而函数bar()是执行一些同步代码的普通函数:

void bar() {
  print("Some synchronous code");
}

现在,假设您的 main() 如下所示:

void main() {
  foo();
  bar();
}

主程序启动并调用foo() - 在main() 中没有await,我们在foo() 中点击await 并且程序运行:“哦!我不应该延迟main() 中的其余执行。我必须注册一个回调以在异步工作完成时执行,然后继续执行 main()"。 foo() 从调用堆栈中弹出,然后bar() 被调用并打印“一些同步工作”,并且也从调用堆栈中弹出。与此同时,foo() 中的异步工作完成并发出完成信号。这被事件循环拾取,该循环返回执行foo() 中的其余代码(或者如果我们使用.then(),则执行回调中的代码;当然,如果主线程不忙。

简而言之,这就是发生的事情。正如 Andrija 所建议的,await 会阻止 同一函数 中的其余代码执行;您的程序的其余部分将运行得很好。如果我们在main() 中使用await 来等待foo(),那么main() 中的执行也会被阻塞,直到foo() 中的异步工作完成,这不是我所拥有的最初的想法。

我的想法是main() 中的代码也会基于foo() 中的await 延迟,而我们看到的情况并非如此。

【讨论】:

  • 看准了!很好的解释!我喜欢你说“我仍然需要考虑很多”——你可以阅读尽可能多的教程或 stackoverflow 答案——但除非你真的“想很多”——否则你不会明白的。还有一个建议——我意识到我一开始会写一个错误的代码——我从来不知道我的逻辑是否错误,或者我处理异步/等待的方式。我意识到——如果我在每一个异步调用上都加上 await,它可以让我专注于我的逻辑。在它工作之后,我会开始通过删除等待来优化性能,并弄清楚如何利用事件循环。
  • 很棒的提示!下次我编写异步代码时,我会尝试应用它。谢谢你:)
【解决方案3】:

其实你的两个函数都有相同的结果,让我解释得更清楚..

当调用 async 函数时,它们只需 don't block 我们应用程序的其他部分进行渲染。无论我们在内部执行什么操作都将是delayed,但其余部分将按原样工作。

现在让我们来看看你的例子

import 'dart:async';

// prints 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1));
  
  print(watch.elapsedMilliseconds); 
  
}

在上面的示例中,您只是传递了delay duration,而不是callback。 因此,它将其余部分视为callback,一旦duration 完成,它将被调用。现在你在做什么,你告诉你的函数等待一个 Duration 你已经提供了execute 进一步的代码。

所以结果是1000+

在下面的例子中

import 'dart:async';

// prints:
// 0
// 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  Future.delayed(Duration(seconds:1)).then((_){print(watch.elapsedMilliseconds);}); // prints 1000+
  
  print(watch.elapsedMilliseconds); // prints 0
  
}

您正确地将callback 分配给Future。所以现在 Future 将只保留它的 callback 让其余部分完成。

这就是它首先打印 0 然后在 delay 之后打印 1000+ 的原因。

Future 和 Future 延迟有不同的工作流程,这可能是现在使用 await 的正确方法。

【讨论】:

  • 仍然,如果您在第一个示例中分配一个回调来打印elapsedMilliseconds,它将打印:1000+,然后再打印 1000+。这表明如果您不使用await,您实际上并没有阻止代码执行。另一方面,如果你使用`await,它会先打印0,然后再打印1000+,这证明代码执行被中断了。
  • 您应该通过链接获取正确的详细信息和 Future 的用例以及示例,它还将让您知道如何正确使用 Future 还考虑您是否以正确的方式使用 Future .请看dart.dev/codelabs/async-await
猜你喜欢
  • 2020-06-19
  • 2019-07-27
  • 2016-02-18
  • 2019-05-14
  • 1970-01-01
  • 1970-01-01
  • 2019-08-17
  • 2021-04-10
  • 2021-03-01
相关资源
最近更新 更多