Day08 | Dart 中的非同步 - Isolate、Event loops

非同步指的到底是什麽?

在解释非同步(Asynchronous)之前,我们先来聊聊什麽是同步(synchronous)
首先我们需要先知道一件事情是:Dart 是一个单执行绪(single thread)语言,也就是一次只做一件事情,而我们的程序在没有非同步事件之前,我们的程序码会逐行执行。而这也就是同步的概念:所有程序都是依序执行,上一个事情做完了我才开始做下一个。

但你也许会想问如果我其中一个事情要做特别久呢?想像一下如果我们程序码全部都是同步的话,我们有一个按钮按下去後会向後端服务器取得资料然後到前端渲染。

大致流程会像这样:按钮点击 → fetch api → 前端重新渲染,假设api server 要两秒後才会吐回资料,在没有非同步的概念下他是会依序执行的,那就会发生我按下按钮後:
在这期间主执行绪是被占满的导致无法进行任何操作, 我的画面会直接停顿两秒 ,这样子就能体会非同步有多重要了吧。

但如果 fetch api 这里是一个非同步的行为,我们这两秒就可以继续滑动画面按其他按钮之类之类,然後两秒後api依然会传资料然後画面重新渲染。

Isolate

简单来说 isolate 就是 Dart 程序码的执行环境,它拥有自己event Loop及记忆体。
我们的 main function 开始执行时也就会先创立一个main isolate 而之後所有的程序码都会在这个isolate 里执行。

虽然在Dart里我们可以自己创建 isolate ,但其实大多数状况我们都只需要一个isolate (也就是main isolate ),直到某个运算真的大到会让我们发生卡顿、掉帧时,我们才需要来考虑使用isolate

因为每个 isolate 就如同他的名字每个都是被隔离的环境,所以我们必须用其他方式来与它进行互动,isolate 间只能透过port交换讯息。

import 'dart:isolate';

void longtimeTask() {
  for (var i = 0; i < 10000000000; i++) {}
  return null;
}

void foo(SendPort sendPort) {
  // 即使有这longtimeTask 也不会阻塞住执行绪
  // 外面的isolateDemo end 马上就会被print
  longtimeTask();
  sendPort.send("foo");
}

void isolateDemo() async {
  print('isolateDemo start');
  final receivePort = ReceivePort();

  final isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort);

  receivePort.listen((data) {
    print('isolate:$data');
    receivePort.close();
    isolate.kill(priority: Isolate.immediate);
  });
  // 将这行打开执行绪会被占据
  // longtimeTask();

  print('isolateDemo end');
}

先宣告一个 ReceivePort 然後 Isolate.spawn 将要执行的function及port传进去,然後在用 receivePort.listen 读取 Isolate 有无讯息传出来。

这边会看到我们的 foo 里有一个 sendPort.send 就是将讯息传出去的方法。而我们这边也放了一个 longtimeTask 来模拟这个Isolate 执行了一个很久的运算。

如果读者实际运行程序码会发现输出的顺序是

isolateDemo start
isolateDemo end
//这篇会停顿两三秒後在输出下面那行
isolate: foo

这时候就能发现 Isolate 将运算隔离出去了,并没有阻塞住执行绪。

如果将注解里的 longtimeTask 打开会发现

isolateDemo start
//这篇会停顿两三秒後在输出下面两行
isolateDemo end
isolate: foo

longtimeTask 将执行去阻塞住导致 isolateDemo end 这边要停顿後才会输出,但也许有人会想那为什麽 foo 这边没有再停顿一次而是直接跟着输出呢?因为这个Isolate 跟我们main Isolate 是隔开的所以他已经在背景运算完并 「通知执行绪已经完成接着等着执行绪执行他的运算结果」 ,但这件事情是如何做到的,就跟接下来要介绍的 Event loops 有十分密切的关系。

Event loops

以网页、App来说这种需要与使用者互动的程序来说,从开始执行到结束的这段时间中,程序本身不会预先知道自己在什麽时间点会被怎样操作,所以这类程序通常会设计会执行在一个 「永不阻塞的单执行绪」(single-threaded & non-blocking) ,然後再利用Event loops的消化这些事件来。

Dart 里的 Event loops 与 JS 相似,有两个 FIFO 的 queue 分别为event queuemicrotask queue:

  • event queue里面存放的有:I/O事件、画面渲染事件、手势事件、与其他isolate互相沟通的事件等等
  • microtask queue :里面存放大部分都是Dart内部的任务,像是清理资源等等我们很少直接操作的事情

先来看一下 Dart Event loops 的流程图

https://ithelp.ithome.com.tw/upload/images/20210921/20112906QLeBP6bbWL.png

从上面这张流程图可以得知Dart是这样执行event loop的:

  1. 执行 main()
  2. 执行 microtask 直到 microtask queue 清空
  3. 执行 「一个」event
  4. 重复 2、3直到 event queue 清空
  5. 结束 app

那这样子到底是如何让单一个isolate(或者可以说执行绪)达成非同步的呢?就如同我们前面说到 「程序本身不会预先知道自己在什麽时间点会被怎样操作」 ,意味着我们的执行绪大多数时候其实都是在 「等待操作」 的,根本原因是因为这是一种被称为 「非阻塞式呼叫」 的概念,当执行这类呼叫时我们的执行绪并不会被占据,直到这个 「非阻塞式呼叫」 的有了结果才会占用到执行绪。

我们可以先来看一下这个简单的范例:

Timer(Duration(seconds: 0), () {
    print('Timer 1');
});
print('normal print 1 ');

这边有一个timer设定为0秒後会执行 print('Timer 1')

及一个一般的 print('normal print 1 ')

这边读者可以先来想想执行结果会是

Timer 1
normal print 1

还是

normal print 1
Timer 1

相信有写过JS的读者应该已经知道答案了。

就是会先输出 normal print 1 之後才是 Timer 1 ,也许有人会有疑问「Timer 0秒不就会是马上执行吗?」

回想一下上面提到的 「非阻塞式呼叫」 ,我们会等到他有结果时,他才会执行虽然是0秒但他是 「0秒後放到执行绪」精确来说是「0秒後放进event queue」,又因为event queue是FIFO所以 normal print 1 就会先被执行到了。

当然有 「非阻塞式呼叫」 就有 「阻塞式呼叫」 ,与 「非阻塞式呼叫」 相反就是在有结果之前都会一直占据着执行绪(Hang 住)。

稍微来整理一下现在提到了几个抽象概念

  1. 同步与非同步

    同步:执行绪执行完一件事之前不能做其他事情,必须一件一件接着做。

    非同步:执行绪执行一件事情在没有得到结果前可以先去做其他事情,之後再回来做。

  2. 阻塞与非阻塞

    阻塞:执行绪执行时会被Hang住。

    非阻塞:执行绪不会被Hang住,但之後会去查询有无结果返回。

所以同步与非同步指的是 「我们使用者与执行绪」的关系,使用者有无必要等结果返回,而阻塞与非阻塞关注的是「执行绪本身的状态」 它自己需不需要等待结果回传,还是要先去做其他事情。

我们现在知道了Dart里有非同步/同步之分,知道了他的好处但感觉就是有点难控制,毕竟我们不知道他何时会回传要是我有事情要等前面的非同步做完才想接着做那我该怎麽办?

其实在 Timer 的例子里就有一个控制非同步的手法叫做 callback ,意思是当这个非同步有了结果後就执行这个 callback function 所以如果,我有好几个要依照顺序做的非同步,我们就会有一个非常巢状的callback,也就是早期JS所说的 「 callback hell」

当然Dart也有提供一些方法让我们更方便的控制非同步,明天我们就要开始进入如何简单的控制这些非同步操作。


今天的程序码也有放到github上

https://github.com/zxc469469/dart-playground/tree/Day08/event-loop


<<:  07 Re: 从零开始的竞程生活

>>:  Day 6 - Vue事件处理

DAY15支持向量机演算法(续四)

昨天介绍完SMO算法第四步,今天就要来写这个方法在迭代中的限制, 基本上每次在计算完Ei之後就要看E...

Day15-守护饼乾大作战(二)

前言 昨天讲了 Secure 跟 HttpOnly 之後,今天要讲的 SameSite 是一个比较复...

你要的是Entity Framework吗?

很多初学Entity Framework( Core)(以下简称EF)的新手,刚开始使用EF时都会有...

npm

昨天安装好Node.js了!所以今天要介绍的是Node.js里面的一个预设的套件,它叫做npm它有点...

SQL 的括号怎麽写成 Laravel Query?

前言 之前工作遇到一个情境需要捞出取消订单状态为 0,1 或没有取消单的订单,然後要再加上其他条件,...