一:前言 - 关于多线程与异步
关于 Dart,我相信大家都知道Dart是一门单线程语言,这里说的单线程并不是说Dart没有或着不能使用多线程,而是Dart的所有API默认情况下都是单线程的。但大家也都知道Dart是有办法支持多线程和异步操作的,关于多线程和异步这两个概念是需要我们理清楚的,不能混淆它们的概念,给我们的理解造成困扰。
1、多线程是开辟另外一个线程来处理事件,每个线程都有单独的事件队列,互不影响,这个新线程和当前线程是并列执行的,有的共享数据空间有的不共享(比如Isolate)。
2、异步是不阻塞当前线程,将异步任务和当前线程的任务分开,异步任务后面的任务,不会等待异步任务执行完再执行,而是直接执行,与异步任务的回调没有关系,这样就不影响当前线程的执行,这就叫异步。
接下来我们按照 事件队列 -- 异步 -- 多线程 这样的顺序整理我们这篇的内容。
二:事件队列
这个和iOS比较类似,在Dart的线程中也存在事件循环和消息队列的概念,在Dart的线程中包含一个事件循环以及两个事件队列,我们先说清楚两个事件队列,再来整理它的事件循环或着说是消息循环机制是什么样子的。
1、事件任务队列(Event Queue):负责处理I/O事件、绘制事件、手势事件、接收其他Isolate消息等外部事件,Timer也是事件队列。
2、微任务队列(Microtask Queue)表示一个短时间内就会完成的异步任务。它的优先级最高,高于Event Queue,只要队列中还有任务,就可以一直霸占着事件循环。Microtask Queue添加的任务主要是由Dart内部产生,当然我们也可以自己添加任务到微任务队列中去,但是我们不要在Microtask Queue里面实现耗时操作避免阻塞Event Queue里的UI事件导致卡顿现象。因为微任务队列的优先级要比事件队列的高,所以事件循环每次循环总是先判断微任务队列中是否有任务需要执行,如果有则先执行微任务队列里的任务,执行完毕之后才会执行事件任务队列里的任务,就会造成卡顿。
具体到两个队列的任务怎么创建添加我们后面再提,在了解了这两个队列之后我们再看看Dart的消息循环机制,下面这张图相信大家都见到过:
关于事件循环的,需要我们特别留意的:
1、在Microtask不为空的时候,Run next Microtask 之后回到最开始,首先判断的是是否还存在微任务,有的话还是优先处理的。
2、在Event不为空的时候,Run next event之后,还是会回去判断是否有Microtask,这点就把前面优先级的问题说的很明白了,这两点需要我们特别留意,在下面我们说完这两个对垒任务的添加之后,我们会写一个稍微比较复杂的方法,仔细的分析一下上面这个事件循环机制。
三:异步
在异步调用中有三个关键词 【async】【await】【Future】,其中async和await/Future是一起使用的,在Dart中可以通过async和await进行一个异步操作,async表示开始一个异步操作,也可以返回一个Future结果。如果没有返回值,则默认返回一个返回值为null的Future,这点也比较容易理解,就像下面的方法,返回值是Future,而我们不写返回return也是可以编译过去的,就是它默认自己返回一个返回值为null的Future。
代码语言:javascript复制Future handleMessage(String message) async {
print(message);
}
Future:默认的Future是异步运行的,也就是把任务放在Future函数体中,这个函数题会被异步执行。
async:异步函数标识,一般与await和Future配合使用。
await:等待异步结果返回,一般加在Future函数体之前,表明后面的代码要等这个Future函数体内的内容执行完在执行,实现同步执行。单独给函数添加async
关键字, 没有意义,函数是否是异步的,主要看Future。
注意:Future<T>通过泛型指定类型的异步操作结果(不需要结果可以使用Future<void>)当一个返回Future对象的函数被调用时,函数将被放入队列等待执行并返回一个未完成的Future对象,等函数操作执行完成时,Future对象变为完成并携带一个值或一个错误。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。
结合上面说的这几点,我们写个实际的小例子:
代码语言:javascript复制class asyncIsolate {
Future<HttpClientRequest> dataReqeust() async {
var httpClient = new HttpClient();
/// var uri = Uri.https('example.org', '/path', {'q': 'dart'});
/// print(uri); // https://example.org/path?q=dart
Future<HttpClientRequest> request =
httpClient.getUrl(Uri.https('jsonplaceholder.typicode.com', '/posts'));
return request;
}
Future<String> loadData() async {
HttpClientRequest request = await dataReqeust();
var response = await request.close();
var responseBody = await response.transform(Utf8Decoder()).join();
print('请求到的数据为:n $responseBody');
return responseBody;
}
}
上面的方法是一个请求数据的小demo,我们调用loadData方法进行数据请求,在运行到loadData内部时候,执行到await会阻塞async内部的执行,从而继续执行外面的代码,一直到dataReqeust的方法有返回,再接着async内部的执行,所以需要知道的事await不会阻塞方法外部代码的执行。
Future可以看做是一个延迟操作的封装,可以将异步任务封装为Future对象。获取到Future对象后,最简单的方法就是用await修饰,并等待返回结果继续向下执行。在Dart
中,和时间相关的操作基本都和Future有关,例如延时操作、异步操作等,下面是一个最简单的延迟操作的例子:
/// 延迟操作
delayedWithFuture() {
DateTime now = DateTime.now();
print("开始时间: $now");
Future.delayed(Duration(seconds: 10), () {
now = DateTime.now();
print("延迟10秒后的时间: $now");
});
/*
flutter: 开始时间: 2022-05-09 13:30:07.164114
flutter: 延迟10秒后的时间: 2022-05-09 13:30:17.171057
*/
}
Dart还支持对Future的链式调用,通过追加一个或多个then方法来实现,这个特性非常实用。例如一个延时操作完成后,会调用then方法,并且可以传递一个参数给then,比如下面的例子:
代码语言:javascript复制delayWithFutureThen() {
Future.delayed(Duration(seconds: 5), () {
int age = 30;
return age;
}).then((onValue) {
onValue ;
print('我多大了啊 $onValue');
});
/*
flutter: 我多大了啊 31
*/
}
Future还有很多有意思的方法,比如 Future.doWhile() 、Future.any()、Future.wait(),我们简单的看一个,比如Future.wait的用法,假设我们有这个一个使用场景,等待在三个Future执行完之后我们还需要执行另外一个Future,这时候我们该怎么处理,下面的demo给了我们处理的方式,注意下输出的日志,我们第一个是延迟的Future,是延迟两秒后输出的。
代码语言:javascript复制///
void awaitWithFuture() async {
Future future1 = Future.delayed(Duration(seconds: 2), () {
print(1);
return 1;
});
Future future2 = Future(() {
print(2);
return 2;
});
Future future3 = Future(() {
print(3);
return 3;
});
Future.wait([future1, future2, future3]).then((value) {
print(value);
}).catchError((error) {});
/*
flutter: 2
flutter: 3
flutter: 1
flutter: [1, 2, 3]
*/
}
微任务队列添加任务,我们通过scheduleMicrotask添加微任务,具体的我们就不在这再写了,的确添加比较简单,接下来我们写一个事件队列和微任务队列在一起的demo,我们梳理一下执行的过程,加深一下对事件循环的理解:
代码语言:javascript复制 analyseWithAsyncTask() {
print('foundation start');
Future.delayed(Duration(seconds: 2), (() {
print('Future - delayed 2 second'); // --- 6
}));
Future(() {
print('Future - 1'); // --- 2
});
Future(() {
print('Future - 2'); // --- 3
});
Timer(Duration(seconds: 3), (() {
print('Timer - delayed 3 second'); // --- 7
}));
scheduleMicrotask((() {
print("Microtask - 1"); // --- 1
Future(() {
print('Future - 3'); // --- 5 【4的Future添加的比Future - 3要早】
});
}));
Future(() {
scheduleMicrotask((() {
print("Microtask - 2"); // --- 4
}));
});
print('foundation end');
}
/*
按照自己理解写出来的执行顺序
先到foundation start 再执行到
Future.delayed(Duration(seconds: 2), (() {
print('Future - delayed 2 second'); // --- 6
}));
没有这个Future添加到事件队列、后面的
Future(() {
print('Future - 2'); // --- 3
});
Timer(Duration(seconds: 3), (() {
print('Timer - delayed 3 second'); // --- 7
}));
也一样,没有先添加到事件队列、接下来是
scheduleMicrotask((() {
print("Microtask - 1"); // --- 1
Future(() {
print('Future - 3'); // --- 5 【4的Future添加的比Future - 3要早】
});
}));
判断微任务队列没有这个任务,添加到微任务队列。再到后面的
Future(() {
scheduleMicrotask((() {
print("Microtask - 2"); // --- 4
}));
});
也一样,也是没有就添加到事件队列,接着就是先打印foundation end
接下来判断有没有优先级更搞得微任务队列是否为空,判断有任务不为空,则执行微任务输出 - Microtask - 1 ,继续执行判断没有事件任务Future - 3
把事件任务添加到事件队列,注意这个事件任务的位置是在标记了// --- 4的事件后面的,执行完判断有没有微任务,发现没有了,开始添加的顺序执行事件任务
就输出了Future - 1 Future - 2 ,执行// --- 4的时候发现微任务,添加到微任务队列,执行下一个事件任务之前,判断有没有微任务,有的话就去执行微任务
就执行了Microtask - 2 ,继续判断微任务空了,继续事件任务。就到了Future - 3 最后两个延时的,安演示正长短 短的先执行
foundation start
foundation end
Microtask - 1
Future - 1
Future - 2
Microtask - 2
Future - 3
Future - delayed 2 second
Timer - delayed 3 second
实际日志输出:
flutter: foundation start
flutter: foundation end
flutter: Microtask - 1
flutter: Future - 1
flutter: Future - 2
flutter: Microtask - 2
flutter: Future - 3
flutter: Future - delayed 2 second
flutter: Timer - delayed 3 second
*/
四: 多线程 - Isolate
Isolate是Dart平台对线程的实现方案,但和普通Thread不同的是,isolate拥有独立的内存,isolate由线程和独立内存构成。正是由于isolate线程之间的内存不共享,所以isolate线程之间并不存在资源抢夺的问题,所以也不需要锁。
通过isolate可以很好的利用多核CPU,来进行大量耗时任务的处理。isolate线程之间的通信主要通过Port来进行,这个Port消息传递的过程是异步的。通过Dart源码也可以看出,实例化一个isolate的过程包括,实例化isolate结构体、在堆中分配线程内存、配置Port等过程。
代码语言:javascript复制 late SendPort subSendPort;
/// 创建新的线程
dispatchQueueAsyncThread() async {
/// 主线程端口
ReceivePort mainThreadPort = ReceivePort();
/// 创建一个新的线程
/*
external static Future<Isolate> spawn<T>(
void entryPoint(T message), T message,
{bool paused = false, | { }里面的这些参数是可选类型的
bool errorsAreFatal = true, |
SendPort? onExit, |
SendPort? onError, |
@Since("2.3") String? debugName}); |
*/
Isolate isolate =
await Isolate.spawn<SendPort>(dataLoader, mainThreadPort.sendPort);
mainThreadPort.listen((message) {
///
print(message);
if (message is SendPort) {
subSendPort = message;
print("子线程创建成功");
print("主线程收到了子线程的ReceivePort的Sendport了,可以通信了");
} else if (message is String) {
if (message == "closed") {
/// 结束这个线程
print('isolate kill');
isolate.kill();
}
}
});
}
/// 主线程发送消息给子线程
mainSendMessageToSubThread() {
if (subSendPort != null) {
subSendPort.send("我是你的主线程");
}
}
/// 主线程发送关闭端口的消息给子线程
/// 子线程关闭接口端口 并告诉主线程 主线程结束子线程
mainSendClosedThreadMessageToSubThread() {
if (subSendPort != null) {
subSendPort.send("close receiveport");
}
}
// 这个SendPort是前面主线程的
static dataLoader(SendPort sendPort) async {
/// 子线程的ReceivePort创建
ReceivePort subThreadPort = ReceivePort();
/// 这是把子线程的ReceivePort的sendPort给了主线程 用于通信
sendPort.send(subThreadPort.sendPort);
subThreadPort.listen((message) {
print("子线程收到的消息 $message");
if (message is String) {
/// 收到主线程让关闭接口的消息 就关闭 正确的话后面是在接受不到消息了
if (message == "close receiveport") {
sendPort.send('closed');
subThreadPort.close();
}
}
});
}
/*
flutter: SendPort
flutter: 子线程创建成功
flutter: 主线程收到了子线程的ReceivePort的Sendport了,可以通信了
flutter: 子线程收到的消息 我是你的主线程
flutter: 子线程收到的消息 close receiveport
flutter: closed
flutter: isolate kill
*/
Isolate的线程更加偏向于底层,在生成一个Isolate之后,其内存是各自独立的,相互之间并不能进行访问,在进行Isolate消息传递的过程中,本质上就是进行Port的传递,通过上面的小例子我们基本上也就掌握了最基础的Flutter消息线程创建和线程之间的消息传递。