Android 17 MessageQueue 无锁化实现解析
目次
最近 Android 有一个比较重磅的变化 —— MessageQueue 的实现从原来的有锁结构改成了无锁结构。
MessageQueue 是 Android 中非常核心的一套机制。 应用中的各种事件都会通过 MessageQueue 来进行排队和分发。
旧的有锁实现,本质上是一个 按时间排序的单链表优先队列。
这里的“优先”并不是传统意义上的 priority,而是:
| |
也就是说 谁应该更早执行,谁就排在前面。
消息按照 when 字段排序:
| |
因此队列始终保证:
| |
MessageQueue.next() 每次只需要取:
| |
即可得到下一条要执行的消息。
这里有一个非常容易被误解的地方:
MessageQueue 并不是 FIFO 队列。
很多人会认为消息是按照发送顺序执行的,但实际上 MessageQueue 是按照 when 排序的。
例如:
| |
发送顺序是:
| |
但 when 的值是:
| |
因此队列顺序变成:
| |
也就是说:执行顺序由时间决定,而不是发送顺序。
所以 MessageQueue 更准确的描述其实是:按执行时间排序的优先队列。
旧实现中:
无论是
- 插入消息
- 获取下一条消息
都需要先获取锁。
基本流程如下:
| |
消费消息时同样需要获取锁。
当消息较多或者多个线程同时插入消息时,就容易出现锁竞争。
同时 UI 渲染相关任务依赖 同步屏障(Sync Barrier)机制 来保证优先级。
当 Choreographer 插入同步屏障后:
| |
新的版本中,将消息处理拆成了两个阶段:
| |
整体结构也发生了变化。
旧实现中生产者线程直接操作有序链表。
而新的实现结构变成:
| |
也就是说:
- 生产者线程
- 只负责把消息压入无锁栈
真正的排序操作只在 Looper 线程消费消息时 才发生。
分发 #
在真正开始分发消息时,Looper 线程会先调用:
| |
把无锁栈中的消息转移到优先队列中。
源码如下:
| |
这里做的事情其实很简单:
| |
完整执行流程如下:
| |
之所以要设计这一层 stack,主要是为了 降低多线程插入消息时的竞争。
如果生产者线程直接操作优先队列,那么每次插入都会涉及:
| |
同时还需要锁保护。
而 stack 的插入是:
| |
并且只需要一次 CAS。
三种状态 #
在新的 MessageQueue 中,涉及到三种状态节点:
| |
分别表示:
| |
例如:
如果下一条消息要在 5 秒后执行,Looper 会进行类似:
| |
的定时等待。
这三种状态会在 下一次插入消息时参与决策。
新的 MessageQueue 内部维护了两个优先队列:
| |
当存在同步屏障时:
| |
如果没有同步屏障,则会从两个队列中选择when 最早的消息执行。
插入消息 #
说完消费流程之后,再来看消息是如何加入队列的。
在新的实现中,消息首先会加入一个 无锁栈。
这个栈实际上是一个用链表实现的 Treiber Stack。
插入逻辑如下:
| |
CAS 的含义是:
| |
只有当当前值等于 expected 时才会更新成功,否则更新失败。
三种状态在插入消息时的作用,就是 决定是否需要唤醒 Looper 线程。
ACTIVE #
| |
此时只需要压栈即可,不需要唤醒。
PARKED #
| |
此时插入新消息必须唤醒:
| |
TIMEDPARK #
| |
此时是否唤醒取决于:
| |
如果更早,就需要提前唤醒。
如果当前栈顶不是状态节点,而是 MessageNode,说明栈中已经堆积了消息。
这时需要通过:
| |
找到栈底的状态节点。
结构类似:
| |
这样 sState 既表示:
| |
如果状态和结构分开维护,就需要:
| |
两次原子操作。
现在只需要:
| |
一次即可保证一致性。
两个队列有什么不同 #
两个优先队列分别是:
- mPriorityQueue
- mAsyncPriorityQueue
插入逻辑如下:
| |
同步消息进入同步队列,异步消息进入异步队列。
这样在存在同步屏障时,可以直接从异步队列取消息,而不需要扫描整个队列。
状态什么时候恢复 Active #
状态恢复为 ACTIVE 的情况主要有:
- Looper线程被唤醒
- Looper进入 next() 开始处理消息
线程重新开始执行消息循环时,会恢复为 ACTIVE 状态。
总结 #
Android 17 对 MessageQueue 做了一次重要的重构。
旧实现结构:
| |
新实现结构:
| |
生产者线程只需要:
| |
消费者线程再统一整理顺序。
同时通过三种状态:
| |
精细控制线程唤醒。
整体思路可以总结为:
| |
这样既降低了锁竞争,也减少了不必要的线程唤醒。
评论
评论由 giscus 提供;如果未加载,可能是网络环境阻止了 giscus。