接上一篇基础语法 ,接下来整理一下Dart的异步和并发。
本文是基于的dart sdk 是stable 2.4.0, devel 2.5.0-dev.1.0
文本分两部分
在JavaScript中,我们也可以使用异步编程模型的回调函数功能
在Dart中你也可以如此
但你会发现,回调函数在可读性、可维护性、以及执行先后顺序等方面均存在问题
于是在Dart中引入了Future和Completer的概念
说到异步, 我们先来看看什么是同步。
举个例子
int getWinningNumber() {
//获取2000以内的一个随机数,表示随机等待的时间
int millisecsToWait = new Random().nextInt(2000);
//获取1970-01-01T00:00:00Z (UTC) 到当前时间的毫秒数
var currentMs = new DateTime.now().millisecondsSinceEpoch;
//Wait
var endMs = currentMs + millisecsToWait;
while (currentMs < endMs) {
currentMs = new DateTime.now().millisecondsSinceEpoch;
}
//返回中奖号码
return new Random().nextInt(32) + 1;
}
void main(){
for (var i = 0; i < 5; i++) {
print(getWinningNumber());
}
print("end");
}
打印结果:
20
11
8
2
28
end
看到了吧, end 就是放在最后才会打印。 也就是说, 主线程(姑且称为主线程)是阻塞,卡死的。
async 库中有一个叫Future的东西。Future是基于观察者模式的。如果你熟悉Rx,你就很容易明白了。
首先先看一下下面的案例,看看它们之间有什么区别?
void testA() async{
new Future(() {
return "This is a doubi";
});
}
Future testB() async{
return new Future(() {
return "This is a doubi";
});
}
Future testC() {
return new Future(() {
return "This is a doubi";
});
}
打印结果: 都是逗比。
再看一下异步:
Future是支持泛型的, 例如Future, 通过T指定将来返回值的类型。
这个函数返回T类型的数据。 在匿名函数中的返回值就是Future的返回值。
当在main函数中 调用getFutureWinningNumber(), 我们通过Future调用then 方法订阅Future, 在then中注册回调函数, 当Future返回值时调用注册函数,当然也可以添加catch 来处理该函数中发生的异常。
void main(){
for (var i = 0; i < 5; i++) {
Future futureNumber = getFutureWinningNumber();
print(futureNumber);/// 打印结果
futureNumber.then((int num)=>print(num));
print("end");
}
}
//并发在Dart中非常的简便,首先声明函数为Future对象,异步传回的结果为int
//计算中奖号码
Future getFutureWinningNumber() {
//Completer就像是控制器,决定Future对象后续的流程,then或者catchError
Completer numberCompleter = new Completer();
//获取中奖号码
Random r = new Random();
int randomNum = r.nextInt(32) + 1;
//这里用Timer模拟一个比较耗时的操作计算
//异步计算的结果,调用函数complete送出结果
new Timer(
new Duration(milliseconds: 2000),
() => numberCompleter.complete(randomNum)
);
return numberCompleter.future;
}
打印结果:
Instance of 'Future'
end
Instance of 'Future'
end
Instance of 'Future'
end
Instance of 'Future'
end
Instance of 'Future'
end
11
13
25
16
11
也就是说主线是不受影响的 直接答应了Future 和 每一次的执行后的end 标识符 。 在then中打印了,执行的结果,也就是在Completer中实现具体的打印方法。
// 2、上面的逻辑可以用wait,执行多个函数后,一并将结果返回为list
void main(){
Future.wait(
[getFutureWinningNumber(),
getFutureWinningNumber(),
getFutureWinningNumber()
]).then((list) {
list.forEach((num) => print(num));
});
print("future.wait end ");
}
打印结果:
future.wait end
16
15
9
是不是很惊喜,类似于Java语言中的CountDownLatch , 所有的异步方法执行完成之后, 然后执行某个操作。
应用场景就比如说 ,所有的图片都上传完毕,弹出消息框告诉用户, 已经上传完成。
//3、异步,效果为顺序显示
getFutureWinningNumber().then((num1) {
print(num1);
//return Future,继续获取中奖号码
//并执行下一个then
return getFutureWinningNumber();
}).then((num2) {
print(num2);
return getFutureWinningNumber();
}).then((num3) {
print(num3);
});
打印结果:
17
32
2
依次打印结果, 如果愿意 ,可以一直这么then下去,这个对于异步任务的时序性要求很高, 比如A、B、C三个任务, 如果想完成B任务, 必须完成A, 要想开始C必须完成B任务。
思考一下,看了上面的案例,对于future的预期行为,如果我们希望在执行其他语句之前,先执行future,该怎么操作呢?
这就需要用到需要用到async/await。在test函数的花括号开始添加async关键字。我们添加await关键字在调用getTest方法之前,他所做的就是在future返回值之后,继续往下执行。我们将整个代码包裹在try-catch中,我们想捕获所有的异常,和之前使用catchError回调是一样。使用awiat关键字,必须给函数添加async关键字,否则没有效果。
注意:要使用 await,其方法必须带有 async 关键字。可以使用 try, catch, 和 finally 来处理使用 await 的异常!
Future test() async {
try {
String value = await getTest();
print("测试----------"+value);
} catch(e) {
print('测试----------Error');
}
print('测试----------逗比是这个先执行吗');
}
Future getTest() {
return new Future.delayed(new Duration(milliseconds: 2000),() {
return "This is a doubi";
});
}
打印结果:
测试------This is a doubi
测试------逗比是这个先执行吗?
很明显在调用async 修饰的方法时候 添加await 关键字会让线程阻塞在这里,等待异步执行的结果。
// 在函数上声明了 async 表明这是一个异步方法
Future doAsync() async {
try {
// 这里是一个模拟请求一个网络耗时操作
var result = await getHttp();
//请求出来的结果
print("print end ");
return printResult(result);
} catch (e) {
print(e);
return false;
}
}
//将请求出来的结果打印出来
Future printResult(summary) {
print(summary);
}
//开始模拟网络请求 等待 5 秒返回一个字符串
getHttp() {
return new Future.delayed(Duration(seconds: 5), () => "Request Succeeded");
}
打印结果:
print end
Request Succeeded
通过在函数主体前添加sync*可以快速标记该函数为同步生成器synchronous generator,解除很多手动定义可迭代类时复杂的公式化代码。假设我们要获取第一个自然数到n:
void main(){
var it = naturalsTo(3).iterator;
while (it.moveNext()) {
print(it.current);
}
}
Iterable naturalsTo(n) sync* {
print("Begin");
int k = 0;
while (k < n) yield k++;
print("End");
}
打印结果:
Begin
0
1
2
End
当调用naturalsTo的时候,会立即返回Iterable(很像async函数立即返回Future),并且可以通过Iterable提取iterator。在有代码调用moveNext前,函数主体并不会执行。yield(生成)用于声明一个求值表达式,当第一次运行函数的时候,代码会执行到yield关键字声明的位置,并暂停执行,moveNext会返回true给调用者。函数会在下次调用moveNext的时候恢复执行。
当循环结束的时候,函数会隐式地执行return,这会使迭代终止,moveNext返回false。当然,作为一个专业的、实现了完整Iterable类API的迭代器和可迭代类来说,使用普通迭代器,这是非常冗长乏味的。
sync 和 * 可以分开,他们是不同的标记。如果你之前的代码使用了sync关键字,升级Dart并不会产生兼容性问题,sync并不是真正的预留字,并同样适用于async、await和yield。这些关键字仅在async函数和generator生成器函数中作为预留字(即:函数用async、sync*、async*标记)。
异步生成器使用数据流来异步生成序列,通过在函数体前添加async*标识符来进行标记。这里同样用生成自然数来举例:
Stream asynchronousNaturalsTo(n) async* {
print("Begin");
int k = 0;
while (k < n) yield k++;
print("End");
}
void main(){
asynchronousNaturalsTo(3).listen((v) {
print(v);
});
}
运行结果:
Begin
0
1
2
End
当运行asynchronousNaturalsTo的时候,会立即返回Stream,但函数体并不会执行,就像sync*生成器和async函数一样。一旦开始listen监听数据流,函数体会开始执行。当执行到yield关键字的时候,会将yield声明的求值表达式的计算结果添加到Stream数据流中。异步生成器没有必要暂停,因为数据流可以通过StreamSubscription进行控制。需注意的是,数据流不能重复监听。
打印结果:
Begin
Before Yield
0
Before Yield
上面的例子中,asynchronousNaturals用于获取小于3的自然数。async等标识符同样适用于get函数。同时,与async函数相关联的Stream可能会被pause暂停或cancel取消。如果被取消,控制权会转让给最近括起来从句末尾,即函数执行完毕。如果被暂停,函数会一直执行到yield声明的位置,然后暂停,直到Stream恢复。
之前的代码中,yield使虽然用起来确实有吸引力,但在写递归函数的时候,也可能遇到一些问题。下面是从大到小获取自然数的例子:
Iterable naturalsDownFrom(n) sync* {
if (n > 0) {
yield n;
for (int i in naturalsDownFrom(n-1)) { yield i; }
}
}
void main(){
print(naturalsDownFrom(3));
}
运行结果:
(3, 2, 1)
由于每次调用sync*函数都会构建一个新的序列,因此在仅使用yield的情况下,只能遍历新构建的序列,通过yield将元素插入到当前序列中:
for (int i in naturalsDownFrom(n-1)) { yield i; }
上面的代码在功能上没什么问题,但我们来分析一下过程:
当n=3时,只执行了一次yield n;;
当n=2时,yield n;和yield i;分别执行了一次,并且执行的yield i;是上一层n=3时候的代码;
当n=1时,执行了一次yield n;,两次yield i;,执行的yield i;是n=2和n=3时候的代码。
在代码中添加提示信息可以看得更明白:
int level = 0;
Iterable naturalsDownFrom(n) sync* {
level++;
if (n > 0) {
print("level: $level n:$n");
yield n;
for (int i in naturalsDownFrom(n-1)) {
print("level: $level i:$i");
yield i;
}
}
}
运行结果:
level: 1 n:3
level: 2 n:2
level: 2 i:2
level: 3 n:1
level: 3 i:1
level: 3 i:1
(3, 2, 1)
为避免和变量名混淆,用x表示自然数的位置(即第x个自然数),y表示yield i;执行的次数,即x=1时,y=0;x=2时,y=1;x=3时,y=2。y的值为0,1,2……x-1。
总的来说,因为递归将元素插入序列的原因,共执行了x(x-1)/2次yield i;,时间复杂度为O(n^2),很明显地存在性能问题,而yield则是为了解决此问题而设计的。yield后面必须跟其他的(子)序列,并且会将后面(子)序列的所有元素插入到当前正在创建的序列中。
于是,代码可以修改如下:
Iterable naturalsDownFrom(n) sync* {
if ( n > 0) {
yield n;
yield* naturalsDownFrom(n-1);
}
}
在sync函数中,子序列必须是一个Iterable可迭代对象;在async函数中,子序列必须是一个Stream数据流。子序列可以为空,当为空的时候,yield*会跳过该表达式,并且不会暂停。
在Dart中有个isolate, isolate 神似Thread , 但实际上两者有本质的区别。操作系统内内的线程之间是可以有共享内存的而isolate没有,这是最为关键的区别。
isolate是Dart对actor并发模式的实现。运行中的Dart程序由一个或多个actor组成,这些actor也就是Dart概念里面的isolate。isolate是有自己的内存和单线程控制的运行实体。isolate本身的意思是“隔离”,因为isolate之间的内存在逻辑上是隔离的。isolate中的代码是按顺序执行的,任何Dart程序的并发都是运行多个isolate的结果。因为Dart没有共享内存的并发,没有竞争的可能性所以不需要锁,也就不用担心死锁的问题。
An isolated Dart execution context. 这是文档对isolate的定义。
由于isolate之间没有共享内存,所以他们之间的通信唯一方式只能是通过Port进行,而且Dart中的消息传递总是异步的。
我们可以看到isolate神似Thread,但实际上两者有本质的区别。操作系统内内的线程之间是可以有共享内存的而isolate没有,这是最为关键的区别。
我们可以阅读Dart源码里面的isolate.cc文件看看isolate的具体实现。
我们可以看到在isolate创建的时候有以下几个主要步骤:
实际上底层还是使用了操作系统提供的OSThread。
将非常耗时的任务添加到事件队列后,仍然会拖慢整个事件循环的处理,甚至是阻塞。可见基于事件循环的异步模型仍然是有很大缺点的,这时候我们就需要Isolate,这个单词的中文意思是隔离。
简单说,可以把它理解为Dart中的线程。但它又不同于线程,更恰当的说应该是微线程,或者说是协程。它与线程最大的区别就是不能共享内存,因此也不存在锁竞争问题,两个Isolate完全是两条独立的执行线,且每个Isolate都有自己的事件循环,它们之间只能通过发送消息通信,所以它的资源开销低于线程。
比个例子吧:
void main(){
print("main isolate start");
create_isolate();
print("main isolate end");
)
// 创建一个新的 isolate
create_isolate() async{
ReceivePort rp = new ReceivePort();
SendPort port1 = rp.sendPort;
Isolate newIsolate = await Isolate.spawn(doWork, port1);
SendPort port2;
rp.listen((message){
print("main isolate message: $message");
if (message[0] == 0){
port2 = message[1];
}else{
port2?.send([1,"这条信息是 main isolate 发送的"]);
}
});
}
// 处理耗时任务
void doWork(SendPort port1){
print("new isolate start");
ReceivePort rp2 = new ReceivePort();
SendPort port2 = rp2.sendPort;
rp2.listen((message){
print("doWork message: $message");
});
// 将新isolate中创建的SendPort发送到主isolate中用于通信
port1.send([0, port2]);
// 模拟耗时5秒
sleep(Duration(seconds:5));
port1.send([1, "doWork 任务完成"]);
print("new isolate end");
}
打印结果:
main isolate start
main isolate end
new isolate start
main isolate message: [0, SendPort]
new isolate end
main isolate message: [1, doWork 任务完成]
doWork message: [1, 这条信息是 main isolate 发送的]
运行后会创建两个进程,一个是主Isolate的进程,一个是新Isolate的进程,两个进程都双向绑定了消息通信的通道,即使新的Isolate中的任务完成了,它的进程也不会立刻退出,因此,当使用完自己创建的Isolate后,最好调用newIsolate.kill(priority: Isolate.immediate);将Isolate立即杀死。
Isolate虽好,但也有合适的使用场景,不建议滥用Isolate,应尽可能多的使用Dart中的事件循环机制去处理异步任务,这样才能更好的发挥Dart语言的优势。
那么应该在什么时候使用Future,什么时候使用Isolate呢?
一个最简单的判断方法是根据某些任务的平均时间来选择:
方法执行在几毫秒或十几毫秒左右的,应使用Future
如果一个任务需要几百毫秒或之上的,则建议创建单独的Isolate
除此之外,还有一些可以参考的场景