Flutter 异步编程:Futures、Isolates、Event Loop[转载]

Dart 是一种单线程语言

首先,大家需要牢记,Dart 是单线程的并且 Flutter 依赖于 Dart。

重点
Dart 同一时刻只执行一个操作,其他操作在该操作之后执行,这意味着只要一个操作正在执行,它就不会被其他 Dart 代码中断。
也就是说,如果你考虑纯粹的同步方法,那么在它完成之前,后者将是唯一要执行的方法。

void myBigLoop(){
    for (int i = 0; i < 1000000; i++){
        _doSomethingSynchronously();
    }
}

在上面的例子中,myBigLoop() 方法在执行完成前永远不会被中断。因此,如果该方法需要一些时间,那么在整个方法执行期间应用将会被阻塞。

Dart 执行模型

那么在幕后,Dart 是如何管理操作序列的执行的呢?

为了回答这个问题,我们需要看一下 Dart 的代码序列器(事件循环)。

当你启动一个 Flutter(或任何 Dart)应用时,将创建并启动一个新的线程进程(在 Dart 中为 「Isolate」)。该线程将是你在整个应用中唯一需要关注的。

所以,此线程创建后,Dart 会自动:

1、初始化 2 个 FIFO(先进先出)队列(「MicroTask」和 「Event」);
2、并且当该方法执行完成后,执行 main() 方法,
3、启动事件循环。
在该线程的整个生命周期中,一个被称为事件循环的单一且隐藏的进程将决定你代码的执行方式及顺序(取决于 MicroTask 和 Event 队列)。

事件循环是一种无限循环(由一个内部时钟控制),在每个时钟周期内,如果没有其他 Dart 代码执行,则执行以下操作:

void eventLoop(){
    while (microTaskQueue.isNotEmpty){
        fetchFirstMicroTaskFromQueue();
        executeThisMicroTask();
        return;
    }

    if (eventQueue.isNotEmpty){
        fetchFirstEventFromQueue();
        executeThisEventRelatedCode();
    }
}

正如我们看到的,MicroTask 队列优先于 Event 队列,那这 2 个队列的作用是什么呢?

MicroTask队列

MicroTask 队列用于非常简短且需要异步执行的内部动作,这些动作需要在其他事情完成之后并在将执行权送还给 Event 队列之前运行。

作为 MicroTask 的一个例子,你可以设想必须在资源关闭后立即释放它。由于关闭过程可能需要一些时间才能完成,你可以按照以下方式编写代码:

MyResource myResource;

...

void closeAndRelease() {
    scheduleMicroTask(_dispose);
    _close();
}

void _close(){
    // 代码以同步的方式运行
    // 以关闭资源
    ...
}

void _dispose(){
    // 代码在
    // _close() 方法
    // 完成后执行
}

这是大多数时候你不必使用的东西。比如,在整个 Flutter 源代码中 scheduleMicroTask() 方法仅被引用了 7 次。

最好优先考虑使用 Event 队列。

Event 队列

Event 队列适用于以下参考模型

  • 外部事件如

    • I/O;
    • 手势;
    • 绘图;
    • 计时器;
    • 流;
      ……
  • futures

事实上,每次外部事件被触发时,要执行的代码都会被 Event 队列所引用。

一旦没有任何 micro task 运行,事件循环将考虑 Event 队列中的第一项并执行它。

值得注意的是,Future 操作也通过 Event 队列处理。

Future

Future 是一个异步执行并且在未来的某一个时刻完成(或失败)的任务。

当你实例化一个 Future 时:

  • 该 future 的一个实例被创建并记录在由 Dart 管理的内部数组中;
  • 需要由此 Future 执行的代码直接推送到 Event 队列中去;
  • 该 future 实例 返回一个状态(= incomplete);
  • 如果存在下一个同步代码,执行它(非 future 的执行代码)
  • 只要事件循环从 Event 循环中获取它,被 future 引用的代码将像其他任何 Event 一样执行。

当该代码将被执行并将完成(或失败)时,then() 或 catchError() 方法将直接被触发。

为了说明这一点,我们来看下面的例子:

void main(){
    print('Before the Future');
    Future((){
        print('Running the Future');
    }).then((_){
        print('Future is complete');
    });
    print('After the Future');
}

如果我们运行该代码,输出将如下所示:

Before the Future
After the Future
Running the Future
Future is complete

执行流程如下:
1、print(‘Before the Future’)
2、将 (){print(‘Running the Future’);} 添加到 Event 队列;
3、print(‘After the Future’)
4、事件循环获取(在第二步引用的)代码并执行它
5、当代码执行时,它会查找 then() 语句并执行它
需要记住一些非常重要的事情:

Future 并非并行执行,而是遵循事件循环处理事件的顺序规则执行。

Async 方法

当你使用 async 关键字作为方法声明的后缀时,Dart 会将其理解为:

  • 该方法的返回值是一个 Future;
  • 它同步执行该方法的代码直到第一个 await 关键字,然后它暂停该方法其他部分的执行;
  • 一旦由 await 关键字引用的 Future 执行完成,下一行代码将立即执行。
    了解这一点是非常重要的,因为很多开发者认为 await 暂停了整个流程直到它执行完成,但事实并非如此。他们忘记了事件循环的运作模式……

为了更好地进行说明,让我们通过以下示例并尝试指出其运行的结果。

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  Future((){                // <== 该代码将在未来的某个时间段执行
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });

  print('C end from $from');
}

methodD(){
  print('D');
}

正确的顺序是:

A
B start
C start from B
C end from B
B end
C start from main
C end from main
D
C running Future from B
C end of Future from B
C running Future from main
C end of Future from main

现在,让我们认为上述代码中的 methodC() 为对服务端的调用,这可能需要不均匀的时间来进行响应。我相信可以很明确地说,预测确切的执行流程可能变得非常困难。

如果你最初希望示例代码中仅在所有代码末尾执行 methodD() ,那么你应该按照以下方式编写代码:

void main() async {
  methodA();
  await methodB();
  await methodC('main');
  methodD();
}

methodA(){
  print('A');
}

methodB() async {
  print('B start');
  await methodC('B');
  print('B end');
}

methodC(String from) async {
  print('C start from $from');

  await Future((){                  // <== 在此处进行修改
    print('C running Future from $from');
  }).then((_){
    print('C end of Future from $from');
  });
  print('C end from $from');
}

methodD(){
  print('D');
}

输出序列为:

A
B start
C start from B
C running Future from B
C end of Future from B
C end from B
B end
C start from main
C running Future from main
C end of Future from main
C
D

事实是通过在 methodC() 中定义 Future 的地方简单地添加 await 会改变整个行为。

另外,需特别谨记:

async 并非并行执行,也是遵循事件循环处理事件的顺序规则执行。

我想向你演示的最后一个例子如下。 运行 method1 和 method2 的输出是什么?它们会是一样的吗?

void method1(){
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  myArray.forEach((String value) async {
    await delayedPrint(value);
  });
  print('end of loop');
}

void method2() async {
  List<String> myArray = <String>['a','b','c'];
  print('before loop');
  for(int i=0; i<myArray.length; i++) {
    await delayedPrint(myArray[i]);
  }
  print('end of loop');
}

Future<void> delayedPrint(String value) async {
  await Future.delayed(Duration(seconds: 1));
  print('delayedPrint: $value');
}

答案:
| method1() | method2() | | --------- | --------- | | 1. before loop | 1. before loop | | 2. end of loop | 2. delayedPrint: a (after 1 second) | | 3. delayedPrint: a (after 1 second) | 3. delayedPrint: b (1 second later) | | 4. delayedPrint: b (directly after) | 4. delayedPrint: c (1 second later) | | 5. delayedPrint: c (directly after) | 5. end of loop (right after) |

你是否清楚它们行为不一样的区别以及原因呢?

答案基于这样一个事实,method1 使用 forEach() 函数来遍历数组。每次迭代时,它都会调用一个被标记为 async(因此是一个 Future)的新回调函数。执行该回调直到遇到 await,而后将剩余的代码推送到 Event 队列。一旦迭代完成,它就会执行下一个语句:“print(‘end of loop’)”。执行完成后,事件循环 将处理已注册的 3 个回调。

对于 method2,所有的内容都运行在一个相同的代码「块」中,因此能够一行一行按照顺序执行(在本例中)。

正如你所看到的,即使在看起来非常简单的代码中,我们仍然需要牢记事件循环的工作方式……

多线程

因此,我们在 Flutter 中如何并行运行代码呢?这可能吗?是的,这多亏了 Isolates

Isolate 是什么?

正如前面解释过的, Isolate 是 Dart 中的 线程。

然而,它与常规「线程」的实现存在较大差异,这也是将其命名为「Isolate」的原因。

「Isolate」在 Flutter 中并不共享内存。不同「Isolate」之间通过「消息」进行通信。

每个 Isolate 都有自己的事件循环

每个「Isolate」都拥有自己的「事件循环」及队列(MicroTask 和 Event)。这意味着在一个 Isolate 中运行的代码与另外一个 Isolate 不存在任何关联。

多亏了这一点,我们可以获得并行处理的能力。

如何启动 Isolate?

根据你运行 Isolate 的场景,你可能需要考虑不同的方法。

1. 底层解决方案

第一个解决方案不依赖任何软件包,它完全依赖 Dart 提供的底层 API。

1.1. 第一步:创建并握手

如前所述,Isolate 不共享任何内存并通过消息进行交互,因此,我们需要找到一种方法在「调用者」与新的 isolate 之间建立通信。

每个 Isolate 都暴露了一个将消息传递给 Isolate 的被称为「SendPort」的端口。(个人觉得该名字有一些误导,因为它是一个接收/监听的端口,但这毕竟是官方名称)。

这意味着「调用者」和「新的 isolate」需要互相知道彼此的端口才能进行通信。这个握手的过程如下所示:

//
// 新的 isolate 端口
// 该端口将在未来使用
// 用来给 isolate 发送消息
//
SendPort newIsolateSendPort;

//
// 新 Isolate 实例
//
Isolate newIsolate;

//
// 启动一个新的 isolate
// 然后开始第一次握手
//
//
void callerCreateIsolate() async {
    //
    // 本地临时 ReceivePort
    // 用于检索新的 isolate 的 SendPort
    //
    ReceivePort receivePort = ReceivePort();

    //
    // 初始化新的 isolate
    //
    newIsolate = await Isolate.spawn(
        callbackFunction,
        receivePort.sendPort,
    );

    //
    // 检索要用于进一步通信的端口
    //
    //
    newIsolateSendPort = await receivePort.first;
}

//
// 新 isolate 的入口
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 一个 SendPort 实例,用来接收来自调用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向调用者提供此 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 进一步流程
    //
}

约束 isolate 的「入口」必须是顶级函数或静态方法。

1.2. 第二步:向 Isolate 提交消息

现在我们有了向 Isolate 发送消息的端口,让我们看看如何做到这一点:

//
// 向新 isolate 发送消息并接收回复的方法
//
//
// 在该例中,我将使用字符串进行通信操作
// (发送和接收的数据)
//
Future<String> sendReceive(String messageToBeSent) async {
    //
    // 创建一个临时端口来接收回复
    //
    ReceivePort port = ReceivePort();

    //
    // 发送消息到 Isolate,并且
    // 通知该 isolate 哪个端口是用来提供
    // 回复的
    //
    newIsolateSendPort.send(
        CrossIsolatesMessage<String>(
            sender: port.sendPort,
            message: messageToBeSent,
        )
    );

    //
    // 等待回复并返回
    //
    return port.first;
}

//
// 扩展回调函数来处理接输入报文
//
static void callbackFunction(SendPort callerSendPort){
    //
    // 初始化一个 SendPort 来接收来自调用者的消息
    //
    //
    ReceivePort newIsolateReceivePort = ReceivePort();

    //
    // 向调用者提供该 isolate 的 SendPort 引用
    //
    callerSendPort.send(newIsolateReceivePort.sendPort);

    //
    // 监听输入报文、处理并提供回复的
    // Isolate 主程序
    //
    newIsolateReceivePort.listen((dynamic message){
        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;

        //
        // 处理消息
        //
        String newMessage = "complemented string " + incomingMessage.message;

        //
        // 发送处理的结果
        //
        incomingMessage.sender.send(newMessage);
    });
}

//
// 帮助类
//
class CrossIsolatesMessage<T> {
    final SendPort sender;
    final T message;

    CrossIsolatesMessage({
        @required this.sender,
        this.message,
    });
}
1.3. 第三步:销毁这个新的 Isolate 实例

当你不再需要这个新的 Isolate 实例时,最好通过以下方法释放它:

//
// 释放一个 isolate 的例程
//
void dispose(){
    newIsolate?.kill(priority: Isolate.immediate);
    newIsolate = null;
}
1.4. 特别说明 - 单监听器流

你可能已经注意到我们正在使用流在「调用者」和新 isolate 之间进行通信。这些流的类型为:「单监听器」流。

2. 一次性计算

如果你只需要运行一些代码来完成一些特定的工作,并且在工作完成之后不需要与 Isolate 进行交互,那么这里有一个非常方便的称为 computeHelper

主要包含以下功能:

  • 产生一个 Isolate
  • 在该 isolate 上运行一个回调函数,并传递一些数据,
  • 返回回调函数的处理结果,
  • 回调执行后终止 Isolate

约束
「回调」函数必须是顶级函数并且不能是闭包或类中的方法(静态或非静态)。

3. 重要限制

在撰写本文时,发现这点十分重要

Platform-Channel 通信仅仅主 isolate 支持。该主 isolate 对应于应用启动时创建的 isolate

也就是说,通过编程创建的 isolate 实例,无法实现 Platform-Channel 通信……

不过,还是有一个解决方法的……请参考此连接以获得关于此主题的讨论。

我应该什么时候使用 Futures 和 Isolate?

用户将根据不同的因素来评估应用的质量,比如:

  • 特性
  • 外观
  • 用户友好性
    ……
    你的应用可以满足以上所有因素,但如果用户在一些处理过程中遇到了卡顿,这极有可能对你不利。

因此,以下是你在开发过程中应该系统考虑的一些点:
1、如果代码片段不能被中断,使用传统的同步过程(一个或多个相互调用的方法);
2、如果代码片段可以独立运行而不影响应用的性能,可以考虑通过 Future 使用事件循环;
3、如果繁重的处理可能需要一些时间才能完成,并且可能影响应用的性能,考虑使用 Isolate。

换句话说,建议尽可能地使用 Future(直接或间接地通过 async 方法),因为一旦事件循环拥有空闲时间,这些 Future 的代码就会被执行。这将使用户感觉事情正在被并行处理(而我们现在知道事实并非如此)。

另外一个可以帮助你决定使用 Future 或 Isolate 的因素是运行某些代码所需要的平均时间。

  • 如果一个方法需要几毫秒 => Future
  • 如果一个处理流程需要几百毫秒 => Isolate

以下是一些很好的 Isolate 选项:

  • JSON 解码:解码 JSON(HttpRequest 的响应)可能需要一些时间 => 使用 compute
  • 加密:加密可能非常耗时 => Isolate
  • 图像处理:处理图像(比如:剪裁)确实需要一些时间来完成 => Isolate
  • 从 Web 加载图像:该场景下,为什么不将它委托给一个完全加载后返回完整图像的 Isolate?
结论

我认为了解事件循环的工作原理非常重要。

同样重要的是要谨记 Flutter(Dart)是单线程的,因此,为了取悦用户,开发者必须确保应用运行尽可能流畅。Future 和 Isolate 是非常强大的工具,它们可以帮助你实现这一目标。

原文地址:Futures - Isolates - Event Loop
原文作者:www.didierboelens.com
译文出自:掘金翻译计划
本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/futures-isolates-event-loop.md
译者:nanjingboy
校对者:sunui, Fengziyin1234

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271