不同于 Java、C++ 使用多线程来实现异步编辑,Dart 没有多线程的概念,它是通过事件循环和隔离来实现异步编程的。
事件循环和隔离
事件循环的原理就是"主线程"循环拉取队列中的事件来执行,如果事件有IO操作或者延时等操作,则会将该事件挂起,并执行下一个任务或者事件。事情循环类似于 kotlin 的指定了线程调度的协程。其运作原理,如下图所示:
可以看到事件循环有两个队列,一个是微任务队列(Microtask queue),另一个是事件队列(Event queue)。其中微任务队列包含Flutter内部的微任务,主要通过scheduleMicrotask来调度;事件队列包含外部事件,例如I/O、Timer和绘制事件等。
为什么 Dart 采用事件循环的单线程异步而不是多线程来实现异步?
- 对于移动/桌面客户端,大多数的需求都是网络、数据库访问等 io 密集型 的任务,使用单线程异步机制就足以满足大部分需求。
- 减少多线程带来的复杂性问题
- 避免了线程创建和销毁的开销,提高了性能。
Dart 如何处理 cpu 密集型任务?
Dart 的单线程异步能方便处理 io 密集型 的任务,但是对于 cpu 密集型任务则需要使用 隔离(isolate
)机制来处理
单线程异步
关于 Dart 的单线程异步,主要有 Future、async和await、Stream 三部分知识点。下面分别介绍
Future
Future 表示一个异步操作的最终完成(或失败)及其结果值的表示。比如说,我们往文件写入文字就会返回一个 Future 对象,如下所示:
dart
File file = File(filePath);
Future<File> futureFile = file.writeAsString("hello world");
可以看到 Future 就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。一些常见的 Future 示例如下:
- Future 链式调用
scss
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print(data);
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});
- Future 等待多个任务完成
scss
Future.wait([
// 2秒后返回结果
Future.delayed(Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
async、await
async
用来表示函数是异步的,定义的函数会返回一个Future
对象,可以使用 then 方法添加回调函数。await
后面是一个Future
,表示等待该异步任务完成,异步完成后才会往下走;await
必须出现在async
函数内部。
代码示例如下:
scss
task() async {
try{
String id = await login("alice","******");
String userInfo = await getUserInfo(id);
await saveUserInfo(userInfo);
//执行接下来的操作
} catch(e){
//错误处理
print(e);
}
}
注意:在 Dart 中,
async/await
只是一个语法糖,编译器或解释器最终都会将其转化为一个 Future 的调用链。
Stream
Stream 也是用于接收异步事件数据,和 Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。代码示例如下:
dart
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
我们还可以使用 Stream 来实现事件流。为了控制事件流,通常使用StreamController来进行管理。例如,为了向事件流中流入数据,StreamController提供了类型为StreamSink的sink属性作为数据的入口,同时StreamController也提供了Stream属性作为数据的出口,如下图所示:
代码示例如下:
scala
class StreamPage extends StatefulWidget {
StreamPage({Key? key, this.title}) : super(key: key);
final String? title;
@override
_StreamState createState() => _StreamState();
}
class _StreamState extends State<StreamPage> {
final StreamController<int> _streamController=StreamController<int>();
int _counter=0;
@override
void dispose() {
_streamController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title ?? ""),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'自动累加的数据',
style: TextStyle(fontSize: 24),
),
StreamBuilder<int>(
stream: _streamController.stream,
initialData: 0,
builder: (BuildContext context,AsyncSnapshot<int> snapshot){
return Text(
'${snapshot.data}',
style: TextStyle(fontSize: 24),
);
},
),
FilledButton(
child: Text(widget.title ?? ""),
onPressed: () => _streamController.sink.add(++_counter),
),
],
),
),
);
}
}
隔离(isolate)
所有的 Flutter Dart 代码都是在隔离上运行的,而对应的Android UI主线程在Flutter中被称为主隔离(main isolate)。我们可以使用 spawnUri
和 spawn
方法创建隔离并执行指定任务。其中 spawnUri
方法,基于给定库的URI来产生一个隔离;而 spawn
方法,根据当前隔离的根库生成一个隔离。代码示例如下:
scss
// 用于与另一个隔离通信
final receivePort = new ReceivePort();
Isolate.spawn(_isolate2, receivePort.sendPort);
// 具体的任务
_isolate2(SendPort replyTo) async {
...
}
这些隔离有着自己的内存和单线程控制的运行实体,因此隔离之间是没有共享内存。如果隔离之间需要通信,则需要消息传递。代码示例如下:
scss
//主隔离
_main() async {
//隔离所需要的参数必须要有SendPort,而SendPort又需要ReceivePort来创建
final receivePort = new ReceivePort();
//使用Isolate.spawn创建隔离,其中_isolate2是我们自己实现的
await Isolate.spawn(_isolate2, receivePort.sendPort);
//发送一个message,这是它的sendPort
var sendPort = await receivePort.first;
var message = await sendMessage(sendPort, "你好");
print("message:$message");//等待消息返回
}
//isolate2
_isolate2(SendPort replyTo) async {
//创建一个ReceivePort,用于接收消息
var port = ReceivePort();
//把它发送给主隔离,以便主隔离可以给它发送消息
replyTo.send(port.sendPort);
port.listen((message) {//监听消息,从Port获取
SendPort send = message[0] as SendPort;
String str = message[1] as String;
print(str);
send.send("应答");
port.close();
});
}
//使用Port进行通信,同时接收返回应答
Future sendMessage(SendPort port,String str) {
ReceivePort receivePort=ReceivePort();
port.send([receivePort.sendPort, str]);
return receivePort.first;
}
如果我们只想要请求一次数据,而不需要像上面一样相互通信,则可以使用 compute
方法。这个函数非常简单,它只有两个参数,第一个参数是需要执行耗时任务的方法的名称,第二个参数是前面方法需要传递的参数。代码示例如下
dart
var _count;
// 耗时任务
int countEven(int num) {
int count = 0;
while (num > 0) {
if (num % 2 == 0) {
count++;
}
num--;
print(count);
}
return count;
}
_call_compute() async{
_count = await compute(countEven, 1000000000);
}
需要注意,compute() 函数中运行的方法必须是顶级方法或者是static方法;而且 compute() 函数只能传递一个参数,它的返回值也只有一个。