共计 8310 个字符,预计需要花费 21 分钟才能阅读完成。
文章主题是“单线程模型下如何保障 UI 的流畅性”。该话题针对的是 Flutter 性能原理开展的,然而 dart 语言就是 js 的延长,很多概念和机制都是一样的。具体不细聊。此外 js 也是单线程模型,在界面展现和 IO 等方面和 dart 相似。所以联合比照讲一下,帮忙梳理和类比,更加容易把握本文的主题,和常识的横向拓展。
先从前端角度登程,剖析下 event loop 和事件队列模型。再从 Flutter 层登程聊聊 dart 侧的事件队列和同步异步工作之间的关系。
一、单线程模型的设计
1. 最根底的单线程解决简略工作
假如有几个工作:
- 工作 1: “ 姓名:” + “ 杭城小刘 ”
- 工作 2: “ 年龄:” + “1995” + “02” + “20”
- 工作 3: “ 大小:” + (2021 – 1995 + 1)
- 工作 4: 打印工作 1、2、3 的后果
在单线程中执行,代码可能如下:
//c
void mainThread () {
string name = "姓名:" + "杭城小刘";
string birthday = "年龄:" + "1995" + "02" + "20"
int age = 2021 - 1995 + 1;
printf("个人信息为:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age);
}
线程开始执行工作,依照需要,单线程顺次执行每个工作,执行结束后线程马上退出。
2. 线程运行过程中来了新的工作怎么解决?
问题 1 介绍的线程模型太简略太现实了,不可能从一开始就 n 个工作就确定了,大多数状况下,会接管到新的 m 个工作。那么 section1 中的设计就无奈满足该需要。
要在线程运行的过程中,可能承受并执行新的工作,就须要有一个事件循环机制。最根底的事件循环能够想到用一个循环来实现。
// c++
int getInput() {
int input = 0;
cout<< "请输出一个数";
cin>>input;
return input;
}
void mainThread () {while(true) {int input1 = getInput();
int input2 = getInput();
int sum = input1 + input2;
print("两数之和为:%d", sum);
}
}
相较于第一版线程设计,这一版做了以下改良:
- 引入了 循环机制,线程不会做完事件马上退出。
- 引入了 事件。线程一开始会期待用户输出,期待的时候线程处于暂停状态,当用户输出结束,线程失去输出的信息,此时线程被激活。执行相加的操作,最终输入后果。一直的期待输出,并计算输入。
3. 解决来自其余线程的工作
实在环境中的线程模块远远没有这么简略。比方浏览器环境下,线程可能正在绘制,可能会接管到 1 个来自用户鼠标点击的事件,1 个来自网络加载 css 资源实现的事件等等。第二版线程模型尽管引入了事件循环机制,能够承受新的事件工作,然而发现没?这些工作之来自线程外部,该设计是无奈承受来自其余线程的工作的。
从上图能够看出,渲染主线程会频繁接管到来自于 IO 线程的一些事件工作,当承受到的资源加载实现后的音讯,则渲染线程会开始 DOM 解析;当接管到来自鼠标点击的音讯,渲染主线程则会执行绑定好的鼠标点击事件脚本(js)来处理事件。
须要一个正当的数据结构,来寄存并获取其余线程发送的音讯?
音讯队列 这个词大家都听过,在 GUI 零碎中,事件队列是一个通用解决方案。
音讯队列(事件队列)是一种正当的数据结构。要执行的工作增加到队列的尾部,须要执行的工作,从队列的头部取出。
有了音讯队列之后,线程模型失去了降级。如下:
能够看出革新分为 3 个步骤:
- 构建一个音讯队列
- IO 线程产生的新工作会被增加到音讯队列的尾部
- 渲染主线程会循环的从音讯队列的头部读取工作,执行工作
伪代码。结构队列接口局部
class TaskQueue {
public:
Task fetchTask (); // 从队列头部取出 1 个工作
void addTask (Task task); // 将工作插入到队列尾部
}
革新主线程
TaskQueue taskQueue;
void processTask ();
void mainThread () {while (true) {Task task = taskQueue.fetchTask();
processTask(task);
}
}
IO 线程
void handleIOTask () {
Task clickTask;
taskQueue.addTask(clickTask);
}
Tips: 事件队列是存在多线程拜访的状况,所以须要加锁。
4. 解决来自其余线程的工作
浏览器环境中,渲染过程常常接管到来自其余过程的工作,IO 线程专门用来接管来自其余过程传递来的音讯。IPC 专门解决跨过程间的通信。
5. 音讯队列中的工作类型
音讯队列中有很多音讯类型。内部消息:如鼠标滚动、点击、挪动、宏工作、微工作、文件读写、定时器等等。
音讯队列中还存在大量的与页面相干的事件。如 JS 执行、DOM 解析、款式计算、布局计算、CSS 动画等等。
上述事件都是在渲染主线程中执行的,因而编码时需注意,尽量减小这些事件所占用的时长。
6. 如何平安退出
Chrome 设计上,确定要退出以后页面时,页面主线程会设置一个退出标记的变量,每次执行完 1 个工作时,判断该标记。如果设置了,则中断工作,退出线程
7. 单线程的毛病
事件队列的特点是先进先出,后进后出。那后进的工作兴许会被后面的工作因为执行工夫过长而阻塞,期待后面的工作执行结束才能够执行前面的工作。这样存在 2 个问题。
-
如何解决高优先级的工作
如果要监控 DOM 节点的变动状况(插入、删除、批改 innerHTML),而后触发对应的逻辑。最根底的做法就是设计一套监听接口,当 DOM 变动时,渲染引擎同步调用这些接口。不过这样子存在很大的问题,就是 DOM 变动会很频繁。如果每次 DOM 变动都触发对应的 JS 接口,则该工作执行会很长,导致 执行效率 的升高
如果将这些 DOM 变动做为异步音讯,如果音讯队列中。可能会存在因为后面的工作在执行导致以后的 DOM 音讯不会被执行的问题,也就是影响了监控的 实时性。
如何衡量效率和实时性?微工作 就是解决该类问题的。
通常,咱们把音讯队列中的工作成为 宏工作 ,每个宏工作中都蕴含一个 微工作队列,在执行宏工作的过程中,如果 DOM 有变动,则该变动会被增加到该宏工作的微工作队列中去,这样子效率问题得以解决。
当宏工作中的次要性能执行结束欧,渲染引擎会执行微工作队列中的微工作。因而实时性问题得以解决
-
如何解决单个工作执行工夫过长的问题
能够看出,如果 JS 计算超时导致动画 paint 超时,会造成卡顿。浏览器为防止该问题,采纳 callback 回调的设计来躲避,也就是让 JS 工作延后执行。
二、flutter 里的单线程模型
1. event loop 机制
Dart 是单线程的,也就是代码会有序执行。此外 Dart 作为 Flutter 这一 GUI 框架的开发语言,必然反对异步。
一个 Flutter 利用蕴含一个或多个 isolate,默认办法的执行都是在 main isolate 中;一个 isolate 蕴含 1 个 Event loop 和 1 个 Task queue。其中,Task queue 蕴含 1 个 Event queue 事件队列和 1 个 MicroTask queue 微工作队列。如下:
为什么须要异步?因为大多数场景下 利用都并不是始终在做运算。比方一边期待用户的输出,输出后再去参加运算。这就是一个 IO 的场景。所以单线程能够再期待的时候做其余事件,而当真正须要解决运算的时候,再去解决。因而虽是单线程,然而给咱们的感触是共事在做很多事件(闲暇的时候去做其余事件)
某个工作波及 IO 或者异步,则主线程会先去做其余须要运算的事件,这个动作是靠 event loop 驱动的。和 JS 一样,dart 中存储事件工作的角色是事件队列 event queue。
Event queue 负责存储须要执行的工作事件,比方 DB 的读取。
Dart 中存在 2 个队列,一个微工作队列(Microtask Queue)、一个事件队列(Event Queue)。
Event loop 一直的轮询,先判断微工作队列是否为空,从队列头部取出须要执行的工作。如果微工作队列为空,则判断事件队列是否为空,不为空则从头部取出事件(比方键盘、IO、网络事件等),而后在主线程执行其回调函数,如下:
2. 异步工作
微工作,即在一个很短的工夫内就会实现的异步工作。微工作在事件循环中优先级最高,只有微工作队列不为空,事件循环就一直执行微工作,后续的事件队列中的工作继续期待。微工作队列可由 scheduleMicroTask
创立。
通常状况,微工作的应用场景比拟少。Flutter 外部也在诸如手势辨认、文本输出、滚动视图、保留页面成果等须要高优执行工作的场景用到了微工作。
所以,个别需要下,异步工作咱们应用优先级较低的 Event Queue。比方 IO、绘制、定时器等,都是通过事件队列驱动主线程来执行的。
Dart 为 Event Queue 的工作提供了一层封装,叫做 Future。把一个函数体放入 Future 中,就实现了同步工作到异步工作的包装(相似于 iOS 中通过 GCD 将一个工作以同步、异步提交给某个队列)。Future 具备链式调用的能力,能够在异步执行结束后执行其余工作(函数)。
看一段具体代码:
void main() {print('normal task 1');
Future(() => print('Task1 Future 1'));
print('normal task 2');
Future(() => print('Task1 Future 2'))
.then((value) => print("subTask 1"))
.then((value) => print("subTask 2"));
}
//
lbp@MBP ~/Desktop dart index.dart
normal task 1
normal task 2
Task1 Future 1
Task1 Future 2
subTask 1
subTask 2
main 办法内,先增加了 1 个一般同步工作,而后以 Future 的模式增加了 1 个异步工作,Dart 会将异步工作退出到事件队列中,而后了解返回。后续代码持续以同步工作的形式执行。而后再增加了 1 个一般同步工作。而后再以 Future 的形式增加了 1 个异步工作,异步工作被退出到事件队列中。此时,事件队列中存在 2 个异步工作,Dart 在事件队列头部取出 1 个工作以同步的形式执行,全副执行(先进先出)结束后再执行后续的 then。
Future 与 then 专用 1 个事件循环。如果存在多个 then,则依照程序执行。
例 2:
void main() {Future(() => print('Task1 Future 1'));
Future(() => print('Task1 Future 2'));
Future(() => print('Task1 Future 3'))
.then((_) => print('subTask 1 in Future 3'));
Future(() => null).then((_) => print('subTask 1 in empty Future'));
}
lbp@MBP ~/Desktop dart index.dart
Task1 Future 1
Task1 Future 2
Task1 Future 3
subTask 1 in Future 3
subTask 1 in empty Future
main 办法内,Task 1 增加到 Future 1 中,被 Dart 增加到 Event Queue 中。Task 1 增加到 Future 2 中,被 Dart 增加到 Event Queue 中。Task 1 增加到 Future 3 中,被 Dart 增加到 Event Queue 中,subTask 1 和 Task 1 共用 Event Queue。Future 4 中工作为空,所以 then 里的代码会被退出到 Microtask Queue,以便下一轮事件循环中被执行。
综合例子
void main() {Future(() => print('Task1 Future 1'));
Future fx = Future(() => null);
Future(() => print("Task1 Future 3")).then((value) {print("subTask 1 Future 3");
scheduleMicrotask(() => print("Microtask 1"));
}).then((value) => print("subTask 3 Future 3"));
Future(() => print("Task1 Future 4"))
.then((value) => Future(() => print("sub subTask 1 Future 4")))
.then((value) => print("sub subTask 2 Future 4"));
Future(() => print("Task1 Future 5"));
fx.then((value) => print("Task1 Future 2"));
scheduleMicrotask(() => print("Microtask 2"));
print("normal Task");
}
lbp@MBP ~/Desktop dart index.dart
normal Task
Microtask 2
Task1 Future 1
Task1 Future 2
Task1 Future 3
subTask 1 Future 3
subTask 3 Future 3
Microtask 1
Task1 Future 4
Task1 Future 5
sub subTask 1 Future 4
sub subTask 2 Future 4
解释:
- Event Loop 优先执行 main 办法同步工作,再执行微工作,最初执行 Event Queue 的异步工作。所以 normal Task 先执行
- 同理微工作 Microtask 2 执行
- 其次,Event Queue FIFO,Task1 Future 1 被执行
- fx Future 外部为空,所以 then 里的内容被加到微工作队列中去,微工作优先级最高,所以 Task1 Future 2 被执行
- 其次,Task1 Future 3 被执行。因为存在 2 个 then,先执行第一个 then 中的 subTask 1 Future 3,而后遇到微工作,所以 Microtask 1 被增加到微工作队列中去,期待下一次 Event Loop 到来时触发。接着执行第二个 then 中的 subTask 3 Future 3。随着下一次 Event Loop 到来,Microtask 1 被执行
- 其次,Task1 Future 4 被执行。随后的第一个 then 中的工作又是被 Future 包装成一个异步工作,被增加到 Event Queue 中,第二个 then 中的内容也被增加到 Event Queue 中。
- 接着,执行 Task1 Future 5。本次事件循环完结
- 等下一轮事件循环到来,打印队列中的 sub subTask 1 Future 4、sub subTask 1 Future 5.
3. 异步函数
异步函数的后果在未来某个时刻才返回,所以须要返回一个 Future 对象,供调用者应用。调用者依据需要,判断是在 Future 对象上注册一个 then 等 Future 执行体完结后再进行异步解决,还是同步等到 Future 执行完结。Future 对象如果须要同步期待,则须要在调用处增加 await,且 Future 所在的函数须要应用 async 关键字。
await 并不是同步期待,而是异步期待。Event Loop 会将调用体所在的函数也当作异步函数,将期待语句的上下文整体增加到 Event Queue 中,一旦返回,Event Loop 会在 Event Queue 中取出上下文代码,期待的代码继续执行。
await 阻塞的是以后上下文的后续代码执行,并不能阻塞其调用栈下层的后续代码执行
void main() {Future(() => print('Task1 Future 1'))
.then((_) async => await Future(() => print("subTask 1 Future 2")))
.then((_) => print("subTask 2 Future 2"));
Future(() => print('Task1 Future 2'));
}
lbp@MBP ~/Desktop dart index.dart
Task1 Future 1
Task1 Future 2
subTask 1 Future 2
subTask 2 Future 2
解析:
- Future 中的 Task1 Future 1 被增加到 Event Queue 中。其次遇到第一个 then,then 外面是 Future 包装的异步工作,所以
Future(() => print("subTask 1 Future 2"))
被增加到 Event Queue 中,所在的 await 函数也被增加到了 Event Queue 中。第二个 then 也被增加到 Event Queue 中 - 第二个 Future 中的 ‘Task1 Future 2 不会被 await 阻塞,因为 await 是异步期待(增加到 Event Queue)。所以执行 ‘Task1 Future 2。随后执行 “subTask 1 Future 2,接着取出 await 执行 subTask 2 Future 2
4. Isolate
Dart 为了利用多核 CPU,将 CPU 层面的密集型计算进行了隔离设计,提供了多线程机制,即 Isolate。每个 Isolate 资源隔离,都有本人的 Event Loop 和 Event Queue、Microtask Queue。Isolate 之间的资源共享通过音讯机制通信(和过程一样)
应用很简略,创立时须要传递一个参数。
void coding(language) {print("hello" + language);
}
void main() {Isolate.spawn(coding, "Dart");
}
lbp@MBP ~/Desktop dart index.dart
hello Dart
大多数状况下,不仅仅须要并发执行。可能还须要某个 Isolate 运算完结后将后果通知主 Isolate。能够通过 Isolate 的管道(SendPort)实现音讯通信。能够在主 Isolate 中将管道作为参数传递给子 Isolate,当子 Isolate 运算完结后将后果利用这个管道传递给主 Isolate
void coding(SendPort port) {
const sum = 1 + 2;
// 给调用方发送后果
port.send(sum);
}
void main() {testIsolate();
}
testIsolate() async {ReceivePort receivePort = ReceivePort(); // 创立管道
Isolate isolate = await Isolate.spawn(coding, receivePort.sendPort); // 创立 Isolate,并传递发送管道作为参数
// 监听音讯
receivePort.listen((message) {print("data: $message");
receivePort.close();
isolate?.kill(priority: Isolate.immediate);
isolate = null;
});
}
lbp@MBP ~/Desktop dart index.dart
data: 3
此外 Flutter 中提供了执行并发计算工作的快捷方式 -compute 函数。其外部对 Isolate 的创立和双向通信进行了封装。
实际上,业务开发中应用 compute 的场景很少,比方 JSON 的编解码能够用 compute。
计算阶乘:
int testCompute() async {return await compute(syncCalcuateFactorial, 100);
}
int syncCalcuateFactorial(upperBounds) => upperBounds < 2
? upperBounds
: upperBounds * syncCalcuateFactorial(upperBounds - 1);
总结:
- Dart 是单线程的,但通过事件循环能够实现异步
- Future 是异步工作的封装,借助于 await 与 async,咱们能够通过事件循环实现非阻塞的同步期待
- Isolate 是 Dart 中的多线程,能够实现并发,有本人的事件循环与 Queue,独占资源。Isolate 之间能够通过音讯机制进行单向通信,这些传递的音讯通过对方的事件循环驱动对方进行异步解决。
- flutter 提供了 CPU 密集运算的 compute 办法,外部封装了 Isolate 和 Isolate 之间的通信
- 事件队列、事件循环的概念在 GUI 零碎中十分重要,简直在前端、Flutter、iOS、Android 甚至是 NodeJS 中都存在。