本文是【从零开始,一块儿学习开发个 Flutter App 吧】路上的第 2 篇文章。java
本文将解决上一篇留下的问题: Dart 中是如何进行异步处理的?咱们首先简单介绍了 Dart 中经常使用的异步处理 Future
、sync
和await
;第二部分试图分析Dart做为单线程语言的异步实现原理,进一步介绍IO模型和事件循环模型;最后介绍 如何在 Dart 实现多线程以线程的相互通讯。node
若是你熟悉 JavaScript 的 Promise 模式的话,发起一个异步http请求,你能够这样写:linux
new Promise((resolve, reject) =>{
// 发起请求
const xhr = new XMLHttpRequest();
xhr.open("GET", 'https://www.nowait.xin/');
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
}).then((response) => { //成功
console.log(response);
}).catch((error) => { // 失败
console.log(error);
});
复制代码
Promise 定义了一种异步处理模式:do... success... or fail...。android
在 Dart 中,与之对应的是Future
对象:web
Future<Response> respFuture = http.get('https://example.com'); //发起请求
respFuture.then((response) { //成功,匿名函数
if (response.statusCode == 200) {
var data = reponse.data;
}
}).catchError((error) { //失败
handle(error);
});
复制代码
这种模式简化和统一了异步的处理,即使没有系统学习过并发编程的同窗,也能够抛开复杂的多线程,开箱即用。数据库
Future
对象封装了Dart 的异步操做,它有未完成(uncompleted)和已完成(completed)两种状态。编程
在Dart中,全部涉及到IO的函数都封装成Future
对象返回,在你调用一个异步函数的时候,在结果或者错误返回以前,你获得的是一个uncompleted
状态的Future
。json
completed
状态也有两种:一种是表明操做成功,返回结果;另外一种表明操做失败,返回错误。缓存
咱们来看一个例子:bash
Future<String> fetchUserOrder() {
//想象这是个耗时的数据库操做
return Future(() => 'Large Latte');
}
void main() {
fetchUserOrder().then((result){print(result)})
print('Fetching user order...');
}
复制代码
经过then
来回调成功结果,main
会先于Future
里面的操做,输出结果:
Fetching user order...
Large Latte
复制代码
在上面的例子中,() => 'Large Latte')
是一个匿名函数,=> 'Large Latte'
至关于 return 'Large Latte'
。
Future
同名构造器是factory Future(FutureOr<T> computation())
,它的函数参数返回值为FutureOr<T>
类型,咱们发现还有不少Future
中的方法好比Future.then
、Future.microtask
的参数类型也是FutureOr<T>
,看来有必要了解一下这个对象。
FutureOr<T>
是个特殊的类型,它没有类成员,不能实例化,也不能够继承,看来它极可能只是一个语法糖。
abstract class FutureOr<T> {
// Private generative constructor, so that it is not subclassable, mixable, or
// instantiable.
FutureOr._() {
throw new UnsupportedError("FutureOr can't be instantiated");
}
}
复制代码
你能够把它理解为受限制的dynamic
类型,由于它只能接受Future<T>
或者T
类型的值:
FutureOr<int> hello(){}
void main(){
FutureOr<int> a = 1; //OK
FutureOr<int> b = Future.value(1); //OK
FutureOr<int> aa = '1' //编译错误
int c = hello(); //ok
Future<int> cc = hello(); //ok
String s = hello(); //编译错误
}
复制代码
在 Dart 的最佳实践里面明确指出:请避免声明函数返回类型为FutureOr<T>
。
若是调用下面的函数,除非进入源代码,不然没法知道返回值的类型到底是int
仍是Future<int>
:
FutureOr<int> triple(FutureOr<int> value) async => (await value) * 3;
复制代码
正确的写法:
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
复制代码
稍微交代了下FutureOr<T>
,咱们继续研究Future
。
若是Future
内的函数执行发生异常,能够经过Future.catchError
来处理异常:
Future<void> fetchUserOrder() {
return Future.delayed(Duration(seconds: 3), () => throw Exception('Logout failed: user ID is invalid'));
}
void main() {
fetchUserOrder().catchError((err, s){print(err);});
print('Fetching user order...');
}
复制代码
输出结果:
Fetching user order...
Exception: Logout failed: user ID is invalid
复制代码
Future
支持链式调用:
Future<String> fetchUserOrder() {
return Future(() => 'AAA');
}
void main() {
fetchUserOrder().then((result) => result + 'BBB')
.then((result) => result + 'CCC')
.then((result){print(result);});
}
复制代码
输出结果:
AAABBBCCC
复制代码
想象一个这样的场景:
接口定义:
Future<String> login(String name,String password){
//登陆
}
Future<User> fetchUserInfo(String token){
//获取用户信息
}
Future saveUserInfo(User user){
// 缓存用户信息
}
复制代码
用Future
大概能够这样写:
login('name','password').then((token) => fetchUserInfo(token))
.then((user) => saveUserInfo(user));
复制代码
换成async
和await
则能够这样:
void doLogin() async {
String token = await login('name','password'); //await 必须在 async 函数体内
User user = await fetchUserInfo(token);
await saveUserInfo(user);
}
复制代码
声明了async
的函数,返回值是必须是Future
对象。即使你在async
函数里面直接返回T
类型数据,编译器会自动帮你包装成Future<T>
类型的对象,若是是void
函数,则返回Future<void>
对象。在遇到await
的时候,又会把Futrue
类型拆包,又会原来的数据类型暴露出来,请注意,await
所在的函数必须添加async
关键词。
await
的代码发生异常,捕获方式跟同步调用函数同样:
void doLogin() async {
try {
var token = await login('name','password');
var user = await fetchUserInfo(token);
await saveUserInfo(user);
} catch (err) {
print('Caught error: $err');
}
}
复制代码
得益于async
和await
这对语法糖,你能够用同步编程的思惟来处理异步编程,大大简化了异步代码的处理。
注:Dart 中很是多的语法糖,它提升了咱们的编程效率,但同时也会让初学者容易感到迷惑。
送多一颗语法糖给你:
Future<String> getUserInfo() async {
return 'aaa';
}
等价于:
Future<String> getUserInfo() async {
return Future.value('aaa');
}
复制代码
Dart 是一门单线程编程语言。对于平时用 Java 的同窗,首先可能会反应:那若是一个操做耗时特别长,不会一直卡住主线程吗?好比Android,为了避免阻塞UI主线程,咱们不得不经过另外的线程来发起耗时操做(网络请求/访问本地文件等),而后再经过Handler来和UI线程沟通。Dart 到底是如何作到的呢?
先给答案:异步 IO + 事件循环。下面具体分析。
咱们先来看看阻塞IO是什么样的:
int count = io.read(buffer); //阻塞等待
复制代码
注: IO 模型是操做系统层面的,这一小节的代码都是伪代码,只是为了方便理解。
当相应线程调用了read
以后,它就会一直在那里等着结果返回,什么也不干,这是阻塞式的IO。
但咱们的应用程序常常是要同时处理好几个IO的,即使一个简单的手机App,同时发生的IO可能就有:用户手势(输入),若干网络请求(输入输出),渲染结果到屏幕(输出);更不用说是服务端程序,成百上千个并发请求都是屡见不鲜。
有人说,这种状况可使用多线程啊。这确实是个思路,但受制于CPU的实际并发数,每一个线程只能同时处理单个IO,性能限制仍是很大,并且还要处理不一样线程之间的同步问题,程序的复杂度大大增长。
若是进行IO的时候不用阻塞,那状况就不同了:
while(true){
for(io in io_array){
status = io.read(buffer);// 无论有没有数据都当即返回
if(status == OK){
}
}
}
复制代码
有了非阻塞IO,经过轮询的方式,咱们就能够对多个IO进行同时处理了,但这样也有一个明显的缺点:在大部分状况下,IO都是没有内容的(CPU的速度远高于IO速度),这样就会致使CPU大部分时间在空转,计算资源依然没有很好获得利用。
为了进一步解决这个问题,人们设计了IO多路转接(IO multiplexing),能够对多个IO监听和设置等待时间:
while(true){
//若是其中一路IO有数据返回,则当即返回;若是一直没有,最多等待不超过timeout时间
status = select(io_array, timeout);
if(status == OK){
for(io in io_array){
io.read() //当即返回,数据都准备好了
}
}
}
复制代码
IO 多路转接有多种实现,好比select、poll、epoll等,咱们不具体展开。
有了IO多路转接,CPU资源利用效率又有了一个提高。
眼尖的同窗可能有发现,在上面的代码中,线程依然是可能会阻塞在 select
上或者产生一些空转的,有没有一个更加完美的方案呢?
答案就是异步IO了:
io.async_read((data) => {
// dosomething
});
复制代码
经过异步IO,咱们就不用不停问操做系统:大家准备好数据了没?而是一有数据系统就会经过消息或者回调的方式传递给咱们。这看起来很完美了,但不幸的是,不是全部的操做系统都很好地支持了这个特性,好比Linux的异步IO就存在各类缺陷,因此在具体的异步IO实现上,不少时候可能会折中考虑不一样的IO模式,好比 Node.js 的背后的libeio
库,实质上采用线程池与阻塞 I/O 模拟出来的异步 I/O [1]。
Dart 在文档中也提到是借鉴了 Node.js 、EventMachine, 和 Twisted 来实现的异步IO,咱们暂不深究它的内部实现(笔者在搜索了一下Dart VM的源码,发如今android和linux上彷佛是经过epoll
实现的),在Dart层,咱们只要把IO当作是异步的就好了。
咱们再回过头来看看上面Future
那段代码:
Future<Response> respFuture = http.get('https://example.com'); //发起请求
复制代码
如今你知道,这个网络请求不是在主线程完成的,它实际上把这个工做丢给了运行时或者操做系统。这也是 Dart 做为单进程语言,但进行IO操做却不会阻塞主线程的缘由。
终于解决了Dart单线程进行IO也不会卡的疑问,但主线程如何和大量异步消息打交道呢?接下来咱们继续讨论Dart的事件循环机制(Event Loop)。
在Dart中,每一个线程都运行在一个叫作isolate
的独立环境中,它的内存不和其余线程共享,它在不停干一件事情:从事件队列中取出事件并处理它。
while(true){
event = event_queue.first() //取出事件
handleEvent(event) //处理事件
drop(event) //从队列中移除
}
复制代码
好比下面这段代码:
RaisedButton(
child: Text('click me');
onPressed: (){ // 点击事件
Future<Response> respFuture = http.get('https://example.com');
respFuture.then((response){ // IO 返回事件
if(response.statusCode == 200){
print('success');
}
})
}
)
复制代码
当你点击屏幕上按钮时,会产生一个事件,这个事件会放入isolate
的事件队列中;接着你发起了一个网络请求,也会产生一个事件,依次进入事件循环。
在线程比较空闲的时候,isolate
还能够去搞搞垃圾回收(GC),喝杯咖啡什么的。
API层的Future
、Stream
、async
和 await
实际都是对事件循环在代码层的抽象。结合事件循环,回到对Future
对象的定义(An object representing a delayed computation.),就能够这样理解了:isolate
大哥,我快递一个代码包裹给你,你拿到后打开这个盒子,并顺序执行里面的代码。
事实上,isolate
里面有两个队列,一个就是事件队列(event queue),还有一个叫作微任务队列(microtask queue)。
事件队列:用来处理外部的事件,若是IO、点击、绘制、计时器(timer)和不一样 isolate 之间的消息事件等。
微任务队列:处理来自于Dart内部的任务,适合用来不会特别耗时或紧急的任务,微任务队列的处理优先级比事件队列的高,若是微任务处理比较耗时,会致使事件堆积,应用响应缓慢。
你能够经过Future.microtask
来向isolate
提交一个微任务:
import 'dart:async';
main() {
new Future(() => print('beautiful'));
Future.microtask(() => print('hi'));
}
复制代码
输出:
hi
beautiful
复制代码
总结一下事件循环的运行机制:当应用启动后,它会建立一个isolate
,启动事件循环,按照FIFO的顺序,优先处理微任务队列,而后再处理事件队列,如此反复。
注:如下当咱们提到isolate的时候,你能够把它等同于线程,但咱们知道它不只仅是一个线程。
得益于异步 IO + 事件循环,尽管Dart是单线程,通常的IO密集型App应用一般也能得到出色的性能表现。但对于一些计算量巨大的场景,好比图片处理、反序列化、文件压缩这些计算密集型的操做,只单靠一个线程就不够用了。
在Dart中,你能够经过Isolate.spawn
来建立一个新的isolate
:
void newIsolate(String mainMessage){
sleep(Duration(seconds: 3));
print(mainMessage);
}
void main() {
// 建立一个新的isolate,newIoslate
Isolate.spawn(newIsolate, 'Hello, Im from new isolate!');
sleep(Duration(seconds: 10)); //主线程阻塞等待
}
复制代码
输出:
Hello, Im from new isolate!
复制代码
spawn
有两个必传参数,第一个是新isolate
入口函数(entrypoint),第二个是这个入口函数的参数值(message)。
若是主isolate
想接收子isolate
的消息,能够在主isolate
建立一个ReceivePort
对象,并把对应的receivePort.sendPort
做为新isolate
入口函数参数传入,而后经过ReceivePort
绑定SendPort
对象给主isolate
发送消息:
//新isolate入口函数
void newIsolate(SendPort sendPort){
sendPort.send("hello, Im from new isolate!");
}
void main() async{
ReceivePort receivePort= ReceivePort();
Isolate isolate = await Isolate.spawn(newIsolate, receivePort.sendPort);
receivePort.listen((message){ //监遵从新isolate发送过来的消息
print(message);
// 再也不使用时,关闭管道
receivePort.close();
// 关闭isolate线程
isolate?.kill(priority: Isolate.immediate);
});
}
复制代码
输出:
hello, Im from new isolate!
复制代码
上面咱们了解了主isolate
是如何监听来自子isolate
的消息的,若是同时子isolate
也想知道主isolate
的一些状态,那该如何处理呢?下面的代码将提供一种双向通讯的方式:
Future<SendPort> initIsolate() async {
Completer completer = new Completer<SendPort>();
ReceivePort isolateToMainStream = ReceivePort();
//监听来自子线程的消息
isolateToMainStream.listen((data) {
if (data is SendPort) {
SendPort mainToIsolateStream = data;
completer.complete(mainToIsolateStream);
} else {
print('[isolateToMainStream] $data');
}
});
Isolate myIsolateInstance = await Isolate.spawn(newIsolate, isolateToMainStream.sendPort);
//返回来自子isolate的sendPort
return completer.future;
}
void newIsolate(SendPort isolateToMainStream) {
ReceivePort mainToIsolateStream = ReceivePort();
//关键实现:把SendPort对象传回给主isolate
isolateToMainStream.send(mainToIsolateStream.sendPort);
//监听来自主isolate的消息
mainToIsolateStream.listen((data) {
print('[mainToIsolateStream] $data');
});
isolateToMainStream.send('This is from new isolate');
}
void main() async{
SendPort mainToIsolate = await initIsolate();
mainToIsolate.send('This is from main isolate');
}
复制代码
输出:
[mainToIsolateStream] This is from main isolatemain end
[isolateToMainStream] This is from new isolate
复制代码
在 Flutter 中,你还能够经过一个简化版的compute
函数启动一个新的isolate
。
好比在反序列化的场景中,直接在主isolate
进行序列化:
List<Photo> parsePhotos(String responseBody) {
final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response =
await client.get('https://jsonplaceholder.typicode.com/photos');
//直接在主isolate转换
return parsePhotos(response.body);
}
复制代码
启动一个新的isolate
:
Future<List<Photo>> fetchPhotos(http.Client client) async {
final response =
await client.get('https://jsonplaceholder.typicode.com/photos');
// 使用compute函数,启动一个新的isolate
return compute(parsePhotos, response.body);
}
复制代码
本示例的完整版:Parse JSON in the background
总结一下,当遇到计算密集型的耗时操做,你能够开启一个新的isolate
来并发执行任务。不像咱们常规认识的多线程,不一样的isolate
之间不能共享内存,但经过ReceivePort
和SendPort
能够构建不一样isolate
之间的消息通道,另外从别的isolate
传来的消息也是要通过事件循环的。
咱们是一支由资深独立开发者和设计师组成的团队,成员均有扎实的技术实力和多年的产品设计开发经验,提供可信赖的软件定制服务。