https://www.dartcn.com/guides/language/language-tour#%E5%BC%82%E6%AD%A5%E6%94%AF%E6%8C%81
https://www.dartcn.com/guides/libraries/library-tour#future
js-异步编程:https://www.yuque.com/zhuchaoyang/wrif6k/zmuxrv
dart 异步编程
因为dart是单线程的语言,所以如果线程中顺序执行的时候遇到一些耗时阻塞的操作,比如数据请求,延时操作等,就会产生卡顿,所以用异步来解决。
//导入io库,调用sleep函数import 'dart:io';import 'dart:async';// 模拟耗时操作,调用sleep,睡眠2秒。doTask() {sleep(Duration(seconds: 2)); //睡眠2秒钟print('do completed!');}main() {print('begin');doTask();print('end');}// begin// 2秒后执行// do completed!// end//后续代码就被阻塞了,必须等异步完成,才能继续执行。
编程中的代码执行,通常分为同步与异步两种。简单说,同步就是按照代码的编写顺序,从上到下依次执行,这也是最简单的我们最常接触的一种形式。但是同步代码的缺点也显而易见,如果其中某一行或几行代码非常耗时,那么就会阻塞,使得后面的代码不能被立刻执行。
异步的出现正是为了解决这种问题,它可以使某部分耗时代码不在当前这条执行线路上立刻执行,那究竟怎么执行呢?最常见的一种方案是使用多线程,也就相当于开辟另一条执行线,然后让耗时代码在另一条执行线上运行,这样两条执行线并列,耗时代码自然也就不能阻塞主执行线上的代码了。
多线程虽然好用,但是在大量并发时,仍然存在两个较大的缺陷,一个是开辟线程比较耗费资源,线程开多了机器吃不消,另一个则是线程的锁问题,多个线程操作共享内存时需要加锁,复杂情况下的锁竞争不仅会降低性能,还可能造成死锁。因此又出现了基于事件的异步模型。简单说就是在某个单线程中存在一个事件循环和一个事件队列,事件循环不断的从事件队列中取出事件来执行,这里的事件就好比是一段代码,每当遇到耗时的事件时,事件循环不会停下来等待结果,它会跳过耗时事件,继续执行其后的事件。当不耗时的事件都完成了,再来查看耗时事件的结果。因此,耗时事件不会阻塞整个事件循环,这让它后面的事件也会有机会得到执行。
我们很容易发现,这种基于事件的异步模型,只适合I/O密集型的耗时操作,因为I/O耗时操作,往往是把时间浪费在等待对方传送数据或者返回结果,因此这种异步模型往往用于网络服务器并发。如果是计算密集型的操作,则应当尽可能利用处理器的多核,实现并行计算。

Dart的事件循环
Dart 是事件驱动的体系结构,该结构基于具有单个事件循环和两个队列的单线程执行模型。 Dart虽然提供调用堆栈。 但是它使用事件在生产者和消费者之间传输上下文。 事件循环由单个线程支持,因此根本不需要同步和锁定。
Dart 的两个队列分别是
MicroTask queue微任务队列Event queue事件队列

Dart事件循环执行如上图所示
- 先查看
MicroTask队列是否为空,不是则先执行MicroTask队列 - 一个
MicroTask执行完后,检查有没有下一个MicroTask,直到MicroTask队列为空,才去执行Event队列 - 在
Evnet队列取出一个事件处理完后,再次返回第一步,去检查MicroTask队列是否为空
我们可以看出,将任务加入到MicroTask中可以被尽快执行,但也需要注意,当事件循环在处理MicroTask队列时,Event队列会被卡住,应用程序无法处理鼠标单击、I/O消息等等事件。
调度任务
注意,以下调用的方法,都定义在dart:async库中。
将任务添加到MicroTask队列有两种方法
import 'dart:async';void myTask() {print("this is my task");}void main() {// 1. 使用 scheduleMicrotask 方法添加scheduleMicrotask(myTask);// 2. 使用Future对象添加new Future.microtask(myTask);}
将任务添加到Event队列
import 'dart:async';void myTask() {print("this is my task");}void main() {new Future(myTask);}
示例:
import 'dart:async';void main() {print(1);new Future(() {print(2);});new Future.microtask(() {print(3);});print(4);}// 1 4 3 2
延时任务
如需要将任务延时执行,则可使用Future.delayed方法。
//表示在延迟时间到了之后将任务加入到Event队列。new Future.delayed(new Duration(seconds: 1), (){print('task delayed');});
注意,这并不是准确的,万一前面有很耗时的任务,那么你的延迟任务不一定能准时运行。
import 'dart:async';import 'dart:io';void main() {print("main start");new Future.delayed(new Duration(seconds: 1), () {print('task delayed');});new Future(() {// 模拟耗时5秒, io的sleep方法sleep(Duration(seconds: 5));print("5s task");});print("main stop");}//main start//main stop//5秒之后打印://5s task//task delayed
从结果可以看出,delayed方法调用在前面,但是它显然并未直接将任务加入Event队列,而是需要等待1秒之后才会去将任务加入,但在这1秒之间,后面的new Future代码直接将一个耗时任务加入到了Event队列,这就直接导致写在前面的delayed任务在1秒后只能被加入到耗时任务之后,只有当前面耗时任务完成后,它才有机会得到执行。这种机制使得延迟任务变得不太可靠,你无法确定延迟任务到底在延迟多久之后被执行。
Future
在 Dart 库中随处可见 Future 对象,通常异步函数返回的对象就是一个 Future。 当一个 future 完成执行后,future 中的值就已经可以使用了。
Future类是对未来结果的一个代理,它返回的并不是被调用的任务的返回值。
void myTask() {print("this is my task");}void main() {Future fut = new Future(myTask);}//Future类实例fut并不是函数myTask的返回值,它只是代理了myTask函数,封装了该任务的执行状态。
处理Future
当你需要完成Future的结果时,你有两个选择:
- 使用async和await
- 使用 Future API
创建 Future
Future的几种创建方法
Future()宏任务Future.microtask()微任务Future.sync()同步Future.value()类似Promise.resolve()- Future.delayed() 延时
Future.error()类似Promise.reject()
示例:在main函数中,我们调用了getAJoke方法,他返回Future<String>.我们通过调用then方法订阅Future,在then中注册回调函数,当Future返回值时调用注册函数。同时注册了catchError方法处理在Future执行之间发生的异常。
import 'dart:async';void main() {// 假设getSome是一个异步请求getSome() {return 'this is a joke';}// 此处 getAJoke() 就是未来的结果Future<String> getAJoke() {return new Future(getSome); //Future的构造函数,需要一个函数作为参数}// 未来的结果获取,使用thengetAJoke().then((res) {print('res => ${res}');}).catchError((err) {print('err => ${err}');});// res => this is a joke}
sync是同步方法,任务会被立即执行
import 'dart:async';void main() {print(1);new Future.sync(() {print(2);});new Future(() {print(3);});print(4);}// 1 2 4 3
注册回调
当Future中的任务完成后,我们往往需要一个回调,这个回调立即执行,不会被添加到事件队列。
then注册回调
import 'dart:async';void main() {print("main start");Future fut = new Future.value(18);// 使用then注册回调fut.then((val) {print('value => ${val}');});// 链式调用,可以跟多个then,注册多个回调new Future(() {print("async task");}).then((res) {print("async task complete");}).then((res) {print("async task after");});print("main stop");}// main start// main stop// value => 18// async task// async task complete// async task after
catchError 异常回调
除了then方法,还可以使用catchError来处理异常,如下:
import 'dart:async';void main() {new Future(() {throw new Exception('异常'); //抛出一个异常}).then((res) {print('res => ${res}');}).catchError((err) {print('error => ${err}');});}// error => Exception: 异常
链式调用
多个网络请求,后者需要前者的返回值当作参数,最后一个才是想要的值。
import 'dart:async';void main() {Future<int> f1() async {return 1;}Future<int> f2(int p) async {return p + 2;}Future<int> f3(p) async {return p + 3;}test() {f1().then((val) => f2(val)).then((val) => f3(val)).then((val) {print(val);});}test(); //6}
Future.wait 并行执行
同时执行多个网络请求,等所有结果都返回后再执行操作,返回一个List的结果集。
import 'dart:async';void main() {print("main start");Future task1 = new Future(() {print("task 1");return 1;});Future task2 = new Future(() {print("task 2");return 2;});Future task3 = new Future(() {print("task 3");return 3;});Future fut = Future.wait([task1, task2, task3]);fut.then((responses) {print('responses => ${responses}');});print("main stop");}// wait返回一个新的Future,当添加的所有Future完成时,在新的Future注册的回调将被执行。// main start// main stop// task 1// task 2// task 3// responses => [1, 2, 3]
async 和 await
在Dart1.9中加入了async和await关键字,有了这两个关键字,我们可以更简洁的编写异步代码,而不需要调用Future相关的API。
将 async 关键字作为方法声明的后缀时,具有如下意义
- 被修饰的方法会将一个
Future对象作为返回值 - 该方法会同步执行其中的方法的代码直到第一个 await 关键字,然后它暂停该方法其他部分的执行;
- 一旦由 await 关键字引用的 Future 任务执行完成,await的下一行代码将立即执行。
// 导入io库,调用sleep函数import 'dart:io';// 模拟耗时操作,调用sleep函数睡眠2秒doTask() async {print('task begin');await sleep(const Duration(seconds: 2));return "task end";}// 定义一个函数用于包装test() async {var r = await doTask();print(r);}void main() {print("main start");test();print("main end");}// main start// task begin// 2秒后输出:// main end// task end
使用 async 和 await 处理 Future
import 'dart:async';void main() {Future<String> getAJoke() {print(3);return new Future.delayed(new Duration(seconds: 2), () {return 'this is a joke';});}print(1);test() async {var aa = await getAJoke();print('test => ${aa}');}test();print(2);}// 1// 3// 2//2秒后输出:// test => this is a joke
注意,async 不是并行执行,它是遵循Dart 事件循环规则来执行的,它仅仅是一个语法糖,简化Future API的使用。
执行顺序
如上所示,在调用函数之后,我们添加了print语句。在这种场景中,print语句会先执行,之后future的返回值才会打印。这是future的预期行为.但是如果我们希望在执行其他语句之前,先执行future。所以我们需要用到async/await.
- 使用new Future将任务加入event队列。
- Future中的then并没有创建新的Event丢到Event Queue中,而只是一个普通的Function Call,在FutureTask执行完后,立即开始执行。
- 如果在then()调用之前Future就已经执行完毕了,那么任务会被加入到microtask队列中,并且该任务会执行then()中注册的回调函数。
- 使用Future.value构造函数的时候,就会上一条一样,创建Task丢到microtask Queue中执行then传入的函数。
- Future.sync构造函数执行了它传入的函数之后,也会立即创建Task丢到microtask Queue中执行。
- 当任务需要延迟执行时,可以使用new Future.delay()来将任务延迟执行。
示例:
void main() {test() {var f = new Future(() => print('f1'));var f1 = Future(() => null);// var f1 = Future.delayed(Duration(seconds: 1), () => null);var f2 = Future(() => null);var f3 = Future(() => null);f3.then((_) => print('f2'));f2.then((_) {print('f3');Future(() => print('f4'));f1.then((_) {print('f5');});});f1.then((m) {print('f6');});print('f7');}test();}// f7// f1// f6// f3// f5// f2// f4
import 'dart:async';void main() {test() {scheduleMicrotask(() => print('s1'));new Future.delayed(new Duration(seconds: 1),() => print('s2')); //因为有延时所以具体执行时间不一定,时间长就最后,时间很短可能在s8后打印new Future(() => print('s3')).then((_) {print('s4');scheduleMicrotask(() => print('s5')); //需要等整个future结束后再去判断Microtask是否有任务}).then((_) => print('s6'));new Future(() => print('s7'));scheduleMicrotask(() => print('s8'));print('s9');}test();}//打印// s9// s1// s8// s3// s4// s6// s5// s7// s2
import 'dart:async';main(List<String> args) async {// async waitgetName1();getName2();getName3();}// async waitFuture<void> getName1() async {print('ssss'); //可以不用await打断点看下await后的区别await getStr1(); //遇到第一个await表达式执行暂停,返回future对象,await表达式执行完成后继续执行await getStr2(); //await表达式可以使用多次print('getName1');}getStr1() {print('getStr1');}getStr2() {print('getStr2');}getName2() {print('getName2');}getName3() {print('getName3');}//打印// ssss// getStr1// getName2// getName3// getStr2// getName1
Isolate
前面已经说过,将非常耗时的任务添加到事件队列后,仍然会拖慢整个事件循环的处理,甚至是阻塞。可见基于事件循环的异步模型仍然是有很大缺点的,这时候我们就需要Isolate,这个单词的中文意思是隔离。
简单说,可以把它理解为Dart中的线程。但它又不同于线程,更恰当的说应该是微线程,或者说是协程。它与线程最大的区别就是不能共享内存,因此也不存在锁竞争问题,两个Isolate完全是两条独立的执行线,且每个Isolate都有自己的事件循环,它们之间只能通过发送消息通信,所以它的资源开销低于线程。
从主Isolate创建一个新的Isolate有两种方法
spawnUri
static Future<Isolate> spawnUri()
spawnUri方法有三个必须的参数:
- 第一个是Uri,指定一个新
Isolate代码文件的路径, - 第二个是参数列表,类型是
List<String>, - 第三个是动态消息。
需要注意,用于运行新Isolate的代码文件中,必须包含一个main函数,它是新Isolate的入口方法,该main函数中的args参数列表,正对应spawnUri中的第二个参数。如不需要向新Isolate中传参数,该参数可传空List
主Isolate中的代码:
import 'dart:isolate';// 创建一个新的 isolatecreate_isolate() async {// 创建主Isolate的ReceivePort对象rpReceivePort rp = new ReceivePort();// 创建rp对象的SendPortSendPort port1 = rp.sendPort;// 通过spawnUri方法,将port1传送到新Isolate的main函数中Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), //第一个参数:指定一个新Isolate代码文件的路径,["hello, isolate", "this is args"], //第二个参数:参数列表,类型是List<String>port1); //第三个参数:动态消息// 创建一个空对象,用于接收rp2对象的SendPortSendPort port2;// 主Isolate创建监听,接收消息rp.listen((message) {print('主Isolate接收到消息: ${message}');if (message[0] == 0) {port2 = message[1]; //把rp2对象的SendPort传递过来} else {// 向新Isolate发送消息port2?.send([1, "这条信息是 主Isolate 发送的"]);}});// 可以在适当的时候,调用以下方法杀死创建的 isolate// newIsolate.kill(priority: Isolate.immediate);}void main() {print('主Isolate开始');create_isolate();print('主Isolate结束');}
创建other_task.dart文件,编写新Isolate的代码
import 'dart:isolate';import 'dart:io';void main(args, SendPort port1) {print('新Isolate开始');print('新Isolate main函数的第一个参数: ${args}');// 创建新Isolate的ReceivePort对象rp2ReceivePort rp2 = new ReceivePort();// 创建rp2对象的SendPortSendPort port2 = rp2.sendPort;// 新Isolate建立监听,接收消息rp2.listen((message) {print('新Isolate接收到消息: ${message}');});// 向主Isolate发送消息,将rp2对象的SendPort发送到主isolate中,用于通信port1.send([0, port2]);// 模拟耗时5秒sleep(Duration(seconds: 5));// 向主Isolate发送消息port1.send([1, '我是新Isolate发送的消息']);print("新Isolate结束");}
运行主Isolate的结果:
主Isolate开始主Isolate结束新Isolate开始新Isolate main函数的第一个参数: [hello, isolate, this is args]主Isolate接收到消息: [0, SendPort]# 5秒后输出:新Isolate结束主Isolate接收到消息: [1, 我是新Isolate发送的消息]新Isolate接收到消息: [1, 这条信息是 主Isolate 发送的]

整个消息通信过程如上图所示,两个Isolate是通过两对Port对象通信,一对Port分别由用于接收消息的ReceivePort对象,和用于发送消息的SendPort对象构成。其中SendPort对象不用单独创建,它已经包含在ReceivePort对象之中。需要注意,一对Port对象只能单向发消息,这就如同一根自来水管,ReceivePort和SendPort分别位于水管的两头,水流只能从SendPort这头流向ReceivePort这头。因此,两个Isolate之间的消息通信肯定是需要两根这样的水管的,这就需要两对Port对象。
理解了Isolate消息通信的原理,那么在Dart代码中,具体是如何操作的呢?
ReceivePort对象通过调用listen方法,传入一个函数可用来监听并处理发送来的消息。SendPort对象则调用send()方法来发送消息。send方法传入的参数可以是null,num, bool, double,String, List ,Map或者是自定义的类。
在上例中,我们发送的是包含两个元素的List对象,第一个元素是整型,表示消息类型,第二个元素则表示消息内容。

spawn
static Future<Isolate> spawn()
除了使用spawnUri,更常用的是使用spawn方法来创建新的Isolate,我们通常希望将新创建的Isolate代码和main Isolate代码写在同一个文件,且不希望出现两个main函数,而是将指定的耗时函数运行在新的Isolate,这样做有利于代码的组织和代码的复用。spawn方法有两个必须的参数,第一个是需要运行在新Isolate的耗时函数,第二个是动态消息,该参数通常用于传送主Isolate的SendPort对象。
spawn的用法与spawnUri相似,且更为简洁,将上面例子稍作修改如下
import 'dart:isolate';import 'dart:io';// 创建一个主Isolatecreate_isolate() async {ReceivePort rp = new ReceivePort();SendPort port1 = rp.sendPort;SendPort port2;// 创建新IsolateIsolate newIsolate = await Isolate.spawn(doWork, port1);rp.listen((message) {print('主Isolate接收到消息: ${message}');if (message[0] == 0) {port2 = message[1]; //把rp2对象的SendPort传递过来} else {// 向新Isolate发送消息port2?.send([1, '这条信息是 主Isolate 发送的']);}});}// 处理耗时任务doWork(port1) {print('新Isolate开始');ReceivePort rp2 = new ReceivePort();SendPort port2 = rp2.sendPort;rp2.listen((message) {print('新Isolate接收到消息: ${message}');});port1.send([0, port2]);sleep(Duration(seconds: 5)); //模拟耗时5秒port1.send([1, "doWork 任务完成"]);print('新Isolate结束');}void main() {print('主Isolate开始');create_isolate();print('主Isolate结束');}
运行结果:
主Isolate开始主Isolate结束新Isolate开始主Isolate接收到消息: [0, SendPort]# 5秒后输出:新Isolate结束主Isolate接收到消息: [1, doWork 任务完成]新Isolate接收到消息: [1, 这条信息是 主Isolate 发送的]
无论是上面的spawn还是spawnUri,运行后都会包含两个Isolate,一个是主Isolate,一个是新Isolate,两个都双向绑定了消息通信的通道,即使新的Isolate中的任务完成了,它也不会立刻退出,因此,当使用完自己创建的Isolate后,最好调用newIsolate.kill(priority: Isolate.immediate);将Isolate立即杀死。
Flutter 中创建Isolate
无论如何,在Dart中创建一个Isolate都显得有些繁琐,可惜的是Dart官方并未提供更高级封装。但是,如果想在Flutter中创建Isolate,则有更简便的API,这是由Flutter官方进一步封装ReceivePort而提供的更简洁API。
详细API文档
使用compute函数来创建新的Isolate并执行耗时任务
import 'package:flutter/foundation.dart';import 'dart:io';// 创建一个新的Isolate,在其中运行任务doWorkcreate_new_task() async{var str = "New Task";var result = await compute(doWork, str);print(result);}String doWork(String value){print("new isolate doWork start");// 模拟耗时5秒sleep(Duration(seconds:5));print("new isolate doWork end");return "complete:$value";}
compute函数有两个必须的参数,第一个是待执行的函数,这个函数必须是一个顶级函数,不能是类的实例方法,可以是类的静态方法,第二个参数为动态的消息类型,可以是被运行函数的参数。需要注意,使用compute应导入'package:flutter/foundation.dart'包。
使用场景
Isolate虽好,但也有合适的使用场景,不建议滥用Isolate,应尽可能多的使用Dart中的事件循环机制去处理异步任务,这样才能更好的发挥Dart语言的优势。
那么应该在什么时候使用Future,什么时候使用Isolate呢?
一个最简单的判断方法是根据某些任务的平均时间来选择:
- 方法执行在几毫秒或十几毫秒左右的,应使用
Future - 如果一个任务需要几百毫秒或之上的,则建议创建单独的
Isolate
除此之外,还有一些可以参考的场景
- JSON 解码
- 加密
- 图像处理:比如剪裁
- 网络请求:加载资源、图片
参考资料:
Dart 文档
Isolate 文档
