Flutter 异步编程指南

作者:京东物流 王志明

1 Dart 中的事件循环模型

在 App 开发中,经常会遇到处理异步任务的场景,如网络请求、读写文件等。Android、iOS 使用的是多线程,而在 Flutter 中为单线程事件循环,如下图所示

Dart 中有两个任务队列,分别为 microtask 队列和 event 队列,队列中的任务按照先进先出的顺序执行,而 microtask 队列的执行优先级高于 event 队列。在 main 方法执行完毕后,会启动事件循环,首先将 microtask 队列中的任务逐个执行完毕,再去执行 event 队列中的任务,每一个 event 队列中的任务在执行完成后,会再去优先执行 microtask 队列中的任务,如此反复,直到清空所有队列,这个过程就是 Dart 事件循环的处理机制。这种机制可以让我们更简单的处理异步任务,不用担心锁的问题。我们可以很容易的预测任务执行的顺序,但无法准确的预测到事件循环何时会处理到你期望执行的任务。例如创建了一个延时任务,但排在前面的任务结束前是不会处理这个延时任务的,也就说这个任务的等待时间可能会大于指定的延迟时间。

Dart 中的方法一旦开始执行就不会被打断,而 event 队列中的事件还来自于用户输入、IO、定时器、绘制等,这意味着在两个队列中都不适合执行计算量过大的任务,才能保证流畅的 UI 绘制和用户事件的快速响应。而且当一个任务的代码发生异常时,只会打断当前任务,后续任务不受影响,程序更不会退出。从上图还可以看出,将一个任务加入 microtask 队列,可以提高任务优先级,但是一般不建议这么做,除非比较紧急的任务并且计算量不大,因为 UI 绘制和处理用户事件是在 event 事件队列中的,滥用 microtask 队列可能会影响用户体验。

总结下 Dart 事件循环的主要概念:

  1. Dart 中有两个队列来执行任务:microtask 队列和 event 队列。
  2. 事件循环在 main 方法执行完毕后启动, microtask 队列中的任务会被优先处理。
  3. microtask 队列只处理来自 Dart 内部的任务,event 队列中有来自 Dart 内部的 Future、Timer、isolate message,还有来自系统的用户输入、IO、UI 绘制等外部事件任务。
  4. Dart 中的方法执行不会被打断,因此两个队列中都不适合用来执行计算量大的任务。
  5. 一个任务中未被处理的异常只会打断当前任务,后续任务不受影响,程序更不会退出。

1.1 向 microtask 队列中添加任务

可以使用顶层方法 scheduleMicrotask 或者 Future.microtask 方法,如下所示:

1scheduleMicrotask(() => print('microtask1')); 2Future.microtask(() => print('microtask2')); 3

使用 Future.microtask 的优势在于可以在 then 回调中处理任务返回的结果。

1.2 向 event 队列中添加任务

1Future(() => print('event task')); 2

基于以上理论,通过如下代码可以验证 Dart 的事件循环机制:

1void main() { 2 print('main start'); 3 4 Future(() => print('event task1')); 5 6 Future.microtask(() => print('microtask1')); 7 8 Future(() => print('event task1')); 9 10 Future.microtask(() => print('microtask2')); 11 12 print('main stop'); 13

执行结果:

1main start 2main stop 3microtask1 4microtask2 5event task1 6event task1 7

通过输出结果可以看到,任务的执行顺序并不是按照编写代码的顺序来的,将任务添加到队列不会立刻执行,而执行顺序也完全符合前面讲的规则,当前 main 方法中的代码执行完毕后,才会去执行队列中的任务,且 microTask 队列的优先级高于 event 队列。

2 Dart 中的异步实现

在 Dart 中通过 Future 来执行异步任务, Future 是对异步任务状态的封装,对任务结果的代理,通过 then 方法可以注册处理任务结果的回调方法。

创建方法 Future 方式:
Future()
Future.delayed()
Future.microtask()
Future.sync()

2.1 Future()

1factory Future(FutureOr<T> computation()) { 2 _Future<T> result = new _Future<T>(); 3 Timer.run(() { 4 try { 5 result._complete(computation()); 6 } catch (e, s) { 7 _completeWithErrorCallback(result, e, s); 8 } 9 }); 10 return result; 11} 12

上面是 Future() 的源码,可以看到内部是通过启动一个没有延迟的计时器来添加任务的,实用 try catch 来捕获任务代码中可能出现的异常,我们可以在 catchError 回调中来处理异常。

2.2 Future.delayed()

1factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) { 2 if (computation == null && !typeAcceptsNull<T>()) { 3 throw ArgumentError.value(null, "computation", "The type parameter is not nullable"); 4 } 5 _Future<T> result = new _Future<T>(); 6 new Timer(duration, () { 7 if (computation == null) { 8 result._complete(null as T); 9 } else { 10 try { 11 result._complete(computation()); 12 } catch (e, s) { 13 _completeWithErrorCallback(result, e, s); 14 } 15 } 16 }); 17 return result; 18} 19

Future.delayed() 与 Future() 的区别是通过一个延迟的计时器来添加任务。

2.3 Future.microtask()

1factory Future.microtask(FutureOr<T> computation()) { 2 _Future<T> result = new _Future<T>(); 3 scheduleMicrotask(() { 4 try { 5 result._complete(computation()); 6 } catch (e, s) { 7 _completeWithErrorCallback(result, e, s); 8 } 9 }); 10 return result; 11} 12

Future.microtask() 是将任务添加到 microtask 队列,通过这种可以很方便通过 then 方法中的回调来处理任务的结果。

2.4 Future.sync()

1factory Future.sync(FutureOr<T> computation()) { 2 try { 3 var result = computation(); 4 if (result is Future<T>) { 5 return result; 6 } else { 7 // TODO(40014): Remove cast when type promotion works. 8 return new _Future<T>.value(result as dynamic); 9 } 10 } catch (error, stackTrace) { 11 var future = new _Future<T>(); 12 AsyncError? replacement = Zone.current.errorCallback(error, stackTrace); 13 if (replacement != null) { 14 future._asyncCompleteError(replacement.error, replacement.stackTrace); 15 } else { 16 future._asyncCompleteError(error, stackTrace); 17 } 18 return future; 19 } 20} 21

Future.sync() 中的任务会被立即执行,不会添加到任何队列。

在第一个章节中讲到了可以很容易的预测任务的执行顺序,下面我们通过一个例子来验证:

1void main() { 2 print('main start'); 3 4 Future.microtask(() => print('microtask1')); 5 6 Future.delayed(new Duration(seconds:1), () => print('delayed event')); 7 Future(() => print('event1')); 8 Future(() => print('event2')); 9 10 Future.microtask(() => print('microtask2')); 11 12 print('main stop'); 13} 14

执行结果:

1main start 2main stop 3microtask1 4microtask2 5event1 6event2 7delayed event 8

因为代码比较简单,通过代码可以很容易的预测到执行结果,下面将复杂度稍微提高。

1void main() { 2 print('main start'); 3 4 Future.microtask(() => print('microtask1')); 5 6 Future.delayed(new Duration(seconds:1), () => print('delayed event')); 7 8 Future(() => print('event1')) 9 .then((_) => print('event1 - callback1')) 10 .then((_) => print('event1 - callback2')); 11 12 Future(() => print('event2')).then((_) { 13 print('event2 - callback1'); 14 return Future(() => print('event4')).then((_) => print('event4 - callback')); 15 }).then((_) { 16 print('event2 - callback2'); 17 Future(() => print('event5')).then((_) => print('event5 - callback')); 18 }).then((_) { 19 print('event2 - callback3'); 20 Future.microtask(() => print('microtask3')); 21 }).then((_) { 22 print('event2 - callback4'); 23 }); 24 25 Future(() => print('event3')); 26 27 Future.sync(() => print('sync task')); 28 29 Future.microtask(() => print('microtask2')).then((_) => print('microtask2 - callbak')); 30 31 print('main stop'); 32} 33

执行结果:

1main start 2sync task 3main stop 4 5microtask1 6microtask2 7microtask2 - callbak 8 9event1 10event1 - callback1 11event1 - callback2 12 13event2 14event2 - callback1 15event3 16 17event4 18event4 - callback 19 20event2 - callback2 21event2 - callback3 22event2 - callback4 23 24microtask3 25event5 26event5 - callback 27 28delayed event 29

看到结果后你可能会疑惑,为什么 event1、event1 - callback1、event1 - callback2 会连续输出,而 event2 - callback1 输出后为什么是 event3,event5、event5 - callback 为什么会在 microtask3 后输出?

这里我们补充下 then 方法的一些关键知识,理解了这些,上面的输出结果也就很好理解了:

  1. then 方法中的回调并不是按照它们注册的顺序来执行。
  2. Future 中的任务执行完毕后会立刻执行 then 方法中的回调,并且回调不会被添加到任何队列中。
  3. 如果 Future 中的任务在 then 方法调用之前已经执行完毕了,那么会有一个任务被加入到 microtask 队列中。这个任务执行的就是被传入then 方法中的回调。

2.5 catchError、whenComplete

1Future(() { 2 throw 'error'; 3}).then((_) { 4 print('success'); 5}).catchError((error) { 6 print(error); 7}).whenComplete(() { 8 print('completed'); 9}); 10

输出结果:

1error 2completed 3

通过 catchError 方法注册的回调,可以用来处理任务代码产生的异常。不管 Future 中的任务执行成功与否,whenComplete 方法都会被调用。

2.6 async、await

使用 async、await 能以更简洁的编写异步代码,是 Dart 提供的一个语法糖。使用 async 关键字修饰的方法返回值类型为 Future,在 async 方法内可以使用 await 关键字来修饰异步任务,在方法内部达到同步执行的效果,可以达到简化代码和提高可读性的效果,不过如果想要处理异常,需要实用 try catch 语句来包裹 await 修饰的异步任务。

1void main() async { 2 print(await getData()); 3} 4 5Future<int> getData() async { 6 final a = await Future.delayed(Duration(seconds: 1), () => 1); 7 final b = await Future.delayed(Duration(seconds: 1), () => 1); 8 return a + b; 9} 10

3 Isolate介绍

前面讲到耗时任务不适合放到 microtask 队列或 event 队列中执行,会导致 UI 卡顿。那么在 Flutter 中有没有既可以执行耗时任务又不影响 UI 绘制呢,其实是有的,前面提到 microtask 队列和 event 队列是在 main isolate 中运行的,而 isolate 是在线程中运行的,那我们开启一个新的 isolate 就可以了,相当于开启一个新的线程,使用多线程的方式来执行任务,Flutter 也为我们提供了相应的 Api。

3.1 compute

1void main() async { 2 compute<String, String>( 3 getData, 4 'Alex', 5 ).then((result) { 6 print(result); 7 }); 8} 9 10String getData(String name) { 11 // 模拟耗时3秒 12 sleep(Duration(seconds: 3)); 13 return 'Hello $name'; 14} 15

compute 第一个参数是要执行的任务,第二个参数是要向任务发送的消息,需要注意的是第一个参数只支持顶层参数。使用 compute() 可以方便的执行耗时任务,但是滥用的话也会适得其反,因为每次调用,相当于新建一个 isolate。上面的代码执行一个经历了 isolate 的创建以及销毁过程,还有数据的传递会经历两次拷贝,因为 isolate 之间是完全隔离的,不能共享内存,整个过程除去任务本身的执行时间,也会非常的耗时,isolate 的创建也比较消耗内存,创建过多的 isolate 还有 OOM 的风险。这时我们就需要一个更优的解决方案,减少频繁创建销毁 isolate 所带来的消耗,最好是能创建一个类似于线程池的东西,只要提前初始化好,后面就可以随时使用,不用担心会发生前面所讲的问题,这时候 LoadBalancer 就派上用场了

3.2 LoadBalancer

1// 用来创建 LoadBalancer 2Future<LoadBalancer> loadBalancerCreator = LoadBalancer.create(2, IsolateRunner.spawn); 3 4// 全局可用的 loadBalancer 5late LoadBalancer loadBalancer; 6 7void main() async { 8 // 初始化 LoadBalancer 9 loadBalancer = await loadBalancerCreator; 10 11 // 使用 LoadBalancer 执行任务 12 final result = await loadBalancer.run<String, String>(getData, 'Alex'); 13 print(result); 14} 15 16String getData(String name) { 17 // 模拟耗时3秒 18 sleep(Duration(seconds: 3)); 19 return 'Hello $name'; 20} 21

使用 LoadBalancer.create() 方法可以创建出一个 isolate 线程池,能够指定 isolate 的数量,并自动实现了负载均衡。应用启动后在合适的时机将其初始化好,后续就有一个全局可用的 LoadBalancer 了。

4 实用经验

4.1 指定任务的执行顺序

在开发中经常会有需要连续执行异步任务的场景,例如下面的例子,后面的一步任务直接需要以来前面任务的结果,所有任务正常执行完毕才算成功。

1void main() async { 2 print(await getData()); 3} 4 5Future<int> getData() { 6 final completer = Completer<int>(); 7 int value = 0; 8 9 Future(() { 10 return 1; 11 }).then((result1) { 12 value += result1; 13 return Future(() { 14 return 2; 15 }).then((result2) { 16 value += result2; 17 return Future(() { 18 return 3; 19 }).then((result3) { 20 value += result3; 21 completer.complete(value); 22 }); 23 }); 24 }); 25 26 return completer.future; 27} 28

这种方式出现了回调地狱,代码非常难以阅读,实际开发中还会有处理异常的代码,会显得更加臃肿,编写难度也大,显然这种方式是不建议使用的。

4.2 使用 then 的链式调用

1void main() async { 2 print(await getData()); 3} 4 5Future<int> getData() { 6 int value = 0; 7 return Future(() => 1).then((result1) { 8 value += result1; 9 return Future(() => 2); 10 }).then((result2) { 11 value += result2; 12 return Future(() => 3); 13 }).then((result3) { 14 value += result3; 15 return value; 16 }); 17} 18

回调地狱的问题解决了,代码可读性提高很多。

4.3 使用 async、await

1void main() async { 2 print(await getData()); 3} 4 5 6Future<int> getData() async { 7 int value = 0; 8 9 value += await Future(() => 1); 10 value += await Future(() => 2); 11 value += await Future(() => 3); 12 13 return value; 14} 15

效果显而易见,代码更加清晰了。

4.4 取消任务

在前面讲到了 Dart 方法执行时是不能被中断的,这就意味着一个 Future 任务开始后必然会走到完成的状态,但是很多时候我们需要又取消一个异步任务,唯一的办法就是在任务结束后不执行回调代码,就可以实现类似取消的效果。

4.5 CancelableOperation

在 Flutter 的 async 包中,提供了一个 CancelableOperation 给我们使用,使用它可以很简单的实现取消任务的需求。

1void main() async { 2 // 创建一个可以取消的任务 3 final cancelableOperation = CancelableOperation.fromFuture( 4 Future(() async { 5 print('start'); 6 await Future.delayed(Duration(seconds: 3)); // 模拟耗时3秒 7 print('end'); 8 }), 9 onCancel: () => print('cancel...'), 10 ); 11 12 // 注册任务结束后的回调 13 cancelableOperation.value.then((val) { 14 print('finished'); 15 }); 16 17 // 模拟1秒后取消任务 18 Future.delayed(Duration(seconds: 1)).then((_) => cancelableOperation.cancel()); 19} 20

CancelableOperation 是对 Future 的代理, 对 Future 的 then 进行了接管,判断 isCanceled 标记决定是否需要执行用户提供的回调。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

PPDB:今晚老齐直播

【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。

VBox 启动虚拟机失败

在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern

FLV文件格式

1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.