dart基础之异步编程

3,113 阅读9分钟

在java中,有Thread来代表一个线程,但是在dart中是没有线程概念的,只有类似于多线程的isolate。除此之外,还针对读文件、数组操作专门制定了一个异步Stream类,使用起来很方便。本次主要分享一下dart中是如何进行异步操作的,这里跟java差别还是蛮大的。

一、isolate

Dart是基于单线程模型的语言。但是在开发当中我们经常会进行耗时操作比如网络请求,这种耗时操作会堵塞我们的代码,所以在Dart也有并发机制,名叫isolate。APP的启动入口main函数就是一个类似Android主线程的一个主isolate。和Java的Thread不同的是,Dart中的isolate无法共享内存,类似于Android中的多进程。

多说无益,直接上代码。如下图,我们首先在主isolate中定义一个receivePort,然后将该receivePort中的sendPort对象以及新的isolate入口方法isolateMain传给了Isolate,相当于告诉新的isolate,你接下来执行这个isolateMain方法吧,和我的通信方式是receivePort.sendPort。接下来再看看isolateMain方法,isolateMain方法中也new了一个receivePort对象传给了主isolate,也是告诉了主线程和它的通信方式是通过这个sendPort来发送信息。具体的发送方式很简单,就是sendPort.send(msg)即可。接收消息是通过刚刚new出来的receivePort.listen()的方式,listen方法的回调参数是一个匿名方法,返回值就是其他isolate传过来的数据。

需要注意的是,由于在不同的isolate中是不共享内存的,这个和Android中的进程有点类似,所以当前isolate中的变量在其他isolate中是不可见的。在本例中,我们在子isolate的入口函数中打印出子isolate中的i变量的值,结果为null,是因为i只在主isolate中赋值为10了,但是子isolate中并没有赋值,也就是null了。那么应该如何让主isolate中的改动让子isolate可见呢,只需要在主isolate中发送一条消息让子isolate修改即可。另外,如果在全局范围内改也是对所有isolate生效的。

二、event-loop

我们首先看一个例子,在单个isolate中由于是在单线程中,所以顺序只能有一个并不能并行。如下例,我们在主isolate中首先注册了监听,然后睡眠了2秒,重点来了,就算在2秒内收到了消息,但是还是会等到那2秒睡眠时间过了以后才会执行收到的消息。最后的结果是首先输出null,然后2秒后再输入休眠完成,然后马上跟上了那2条消息。



同Android Handler类似,在Dart运行环境中也是靠事件驱动的,通过event loop不停的从队列中获取消息或者事件来驱动整个应用的运行,isolate发过来的消息就是通过loop处理。但是不同的是在Android中每个线程只有一个Looper所对应的MessageQueue,而Dart中有两个队列,一个叫做event queue(事件队列),另一个叫做microtask queue(微任务队列)。其中上面的例子中使用的是事件队列,事件队列的优先级不如微任务队列。

Dart在执行完main函数后,才会由Loop开始执行两个任务队列中的Event,这就是main方法内sleep会影响回调执行的原因。首先Loop检查微服务队列,依次执行Event,当微服务队列执行完后,就检查Event queue队列依次执行,在执行Event queue的过程中,每执行完一个Event就再检查一次微服务队列。所以微服务队列优先级高,可以利用微服务进行插队。对于这个特性,我们可以再看一个例子。在该例子中,主isolate中有一个死循环,所以导致loop并不会去检查事件队列,所以这个输出永远都出不来,将一直卡在main()中。

我们再来验证一下微服务的插队功能,如下例,首先调用then方法将读文件加入到事件队列,然后开启了一个微服务。在这里执行的顺序是main->微服务->事件队列,所以结果是先输出future:excute microtask,然后输出被读文件中的内容。


三、future

在 Dart 库中随处可见 Future 对象,通常异步函数返回的对象就是一个 Future。 当一个 future 执行完后,他里面的值 就可以使用了,可以使用 then() 来在 future 完成的时候执行其他代码。Future对象其实就代表了在事件队列中的一个事件的结果。

可能看这个说明有点晕乎,我们看一下上面读文件那个例子中的then方法返回值吧,看到没返回值就是Future对象。也就是说事件队列的结果就是一个future,这里之所以拿出来讲是因为dart中使用到的地方实在太多了。接下来一起看看future类中都提供了哪些功能吧。


1.异常捕获

通过future类可以捕获到时间队列中的错误,通过catchError方法来获取到回调。我们知道在主Isolate中捕获异常可以用try...catch,其实catchError可以看做是异步的try...catch。

2.组合

then()的返回值同样是一个future对象,可以利用队列的原理进行组合异步任务。在本例中,先执行文件读取操作,然后再将返回值1进行输出,最后抓一下错误。相当于类似build模式,then可以一直点下去,上次的返回值就是本次的参数。

上面是一个一个任务执行,还有一种操作是等待多个任务都完成以后再执行其他操作。如下例,我们定义了3个任务,第一个是读文件,第二个是延迟3秒,第三个是输出一些内容。由于使用了Future.wait方法将前2个任务绑定,所以结果是会等前2个任务完成以后才会执行第三个


四、Stream

Future 表示稍后获得的一个数据,所有异步的操作的返回值都用 Future 来表示。但是 Future 只能表示一次异步获得的数据。而 Stream 表示多次异步获得的数据。比如 IO 处理的时候,每次只会读取一部分数据和一次性读取整个文件的内容相比,Stream 的好处是处理过程中内存占用较小。而 File 的readAsString()是一次性读取整个文件的内容进来,虽然获得完整内容处理起来比较方便,但是如果文件很大的话就会导致内存占用过大的问题。

补充个例子可能更加具体,使用openRead来打开Stream流,然后监听流的变化,最后执行了多次的回调。当然读取的文件要足够大,如果小的话也只会读取一次的。接下来继续使用future,以此来读取,两种方式都是可以的。


还有一个特性是可以使用onData来对监听进行重置。如下例,获取到监听者对象后,对监听方法进行了替换,最后执行的是新方法,而老方法直接被丢弃。除了替换监听方法,还可以执行其他的方法,比如调用onDone来结束一个监听。



五、广播

Stream有两种订阅模式:单订阅和多订阅。单订阅就是只能有一个订阅者,上面的使用我们都是单订阅模式,而广播是可以有多个订阅者。通过 Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream,isBroadcast 属性可以判断当前 Stream 所处的模式。

如下例,通过将asBroadcastStream将Stream转换成广播,然后就可以多处监听了。


需要注意的是,如果是直接创建的流管理器,就算是多订阅模式下,如果先发送然后再注册,是不会接收到消息的。


但是如果是通过asBroadcastStream来获取到streamCOntroller,先发送再注册,也是会接收到消息的。利用这个特性,可以用来实现sticky粘性广播。


六、async/await

使用'async'和'await'的代码是异步的,但是看起来很像同步代码。当我们需要获得A的结果,再执行B,时,你需要'then()->then()',但是利用'async'与'await'能够非常好的解决回调地狱的问题。

在下例中,我们将readFile用async进行修饰,代表该方法将使用同步关键字await。接下来我们读取了2次文件,都用await修饰,代表这两次操作都以同步的方式运行,只有在第一行执行完以后才进入第二行代码的执行。


最后,给大家带来使用stream手撸一个eventbus的代码。在注册时将Stream传出到调用处,然后就可以调用Stream的listen方法来获取消息。发送消息只需要调用StreamController的add方法即可,由于考虑到需要往不同的事件中发送消息,所以用了一个map来保存所有的订阅类型。另外,为了方便调用,这里使用了一个单例模式,对外公开了getInstance方法用来将单例对象传递给外界。取消注册是先关闭需要取消订阅的streamcontroller,然后从map移除


总结:本次介绍了异步操作isolate的使用,以及dart中事件队列、微服务队列的相关知识,然后了解了future对象是事件队列的结果,接下来对比了stream和future的区别,然后介绍了广播的直接创建、stream创建方式以及区别,接下来又介绍了async、await的结合使用方法,最后分享了自己的一个异步实战-使用Stream手写Eventbus。