1. 课程导读:为什么轮询(Polling)会导致 L0 卡顿?一个系统层面的故事

大家好,我是你们的老朋友。今天咱们开始聊一个很有意思的话题——轮询。说白了,轮询就是让 CPU 像个巡逻兵一样,不停地去问某个设备:“你准备好了吗?”

这种机制,我最早接触是在做嵌入式开发的时候。当时觉得,轮询多简单啊,代码写起来直来直去,调试也方便。但后来随着系统复杂度上去,我才发现,这玩意儿简直是性能杀手。

1.1 什么是轮询?

轮询,就是程序主动去检查某个条件是否满足。代码长这样:

while (1) {
    if (check_device_ready()) {
        process_data();
    }
    // 如果没有准备好,CPU 就在这里空转
}

你想想看,这种写法有什么问题?CPU 在空转的时候,其实什么都没干,但一直在耗电、占着核心。这在单任务系统里还行,但在现代操作系统中,简直就是灾难。

核心问题:轮询等于用 CPU 时间片去换一个不确定的等待。这个等待如果是微秒级还好,如果是毫秒级,那性能损失就大了去了。

1.2 什么是 L0 卡顿?

L0 卡顿,是性能优化里最让人头疼的一种。我个人的理解是:L0 卡顿就是用户能直接感知到的、毫无预兆的停顿。比如你在手机上滑动页面,突然卡了 200 毫秒,这就是一次 L0 卡顿。

它的特点是:

  • 可感知:用户能明显感觉到“卡了一下”
  • 不可预测:不是每次都出现,随机性强
  • 难以复现:测试环境很难百分百复现

我在项目中遇到过这样一个场景:一个看似正常的轮询操作,在低负载下没问题,但一到高负载场景,L0 卡顿就频繁出现。当时查了好久才发现,是轮询占用了本该给 UI 线程的时间片。

1.3 从系统层面看轮询的原理

咱们来画一张图。这张图展示了轮询和中断两种方式的区别。

轮询 vs 中断:CPU 时间片对比 轮询方式 CPU 执行主任务 CPU 空转(轮询等待) CPU 执行主任务 CPU 空转(轮询等待) CPU 执行主任务 CPU 空转(轮询等待) → 时间轴 → 中断方式 CPU 执行主任务 CPU 执行主任务 CPU 执行主任务 主任务 中断 主任务 CPU 执行主任务 CPU 执行主任务 → 时间轴 → CPU 利用率差异 关键点:轮询导致 CPU 在等待期间无法处理其他任务,而中断只在事件发生时短暂介入 L0 卡顿往往就出现在轮询占用了 CPU 时间片,导致 UI 线程得不到调度的时候。

看到这张图,你应该能理解为什么轮询会导致性能问题了。左侧的轮询方式中,CPU 有大量时间是在空转等待。右侧的中断方式,CPU 大部分时间都在干正事,只有事件发生时才会中断一下。

1.4 轮询引发 L0 卡顿的传导路径

咱们来拆解一下,轮询是怎么一步步导致 L0 卡顿的。我个人总结了这么几条路径:

  1. CPU 时间片被蚕食:轮询循环会消耗 CPU 时间片。如果轮询间隔很短,比如 1 毫秒一次,那 CPU 每秒要检查 1000 次。每次检查虽然只有几微秒,但累积起来就很可观了。
  2. 线程调度延迟:当系统中有高优先级线程(比如 UI 线程)需要运行时,轮询线程如果还霸占着 CPU,就会导致高优先级线程被延迟。
  3. 缓存污染:轮询频繁访问内存或外设寄存器,会导致 CPU 缓存被频繁刷新。我遇到过的情况是,一个轮询操作把 L1 缓存全都污染了,导致后续的主任务需要重新加载数据。
  4. 中断被屏蔽:有些轮询实现会关中断,或者设置很长的临界区。这时候如果有真正紧急的事件来了,反而被耽搁了。

避坑指南:我曾经犯过一个错误——在 UI 主线程里做了一个简单的轮询,每次只检查一个标志位,觉得“就几行代码,没什么影响”。结果呢?那个轮询导致 UI 线程每 5 毫秒被阻塞一次,用户操作时感觉“黏黏的”,但其实还没到卡顿的地步。这种问题最难排查,因为你不开性能分析工具根本发现不了。

1.5 轮询的系统层面代价

我们来量化一下轮询的代价。假设一个定时器轮询的实现:

轮询间隔 每次耗时 每秒消耗 CPU 占用率 (2GHz 核心)
1ms 2μs 2000μs 0.4%
100μs 2μs 20000μs 4%
10μs 2μs 200000μs 40%

看到没?当轮询间隔缩短到 10 微秒时,CPU 占用率就飙升到了 40%。而这 40% 的 CPU 其实什么都没干,就是在傻等。这 40% 原本可以用来处理 UI 渲染、网络数据、后台任务,现在全浪费了。

你想想看,如果一个手机上有三四个这样的轮询,那 CPU 就基本没空干正事了。L0 卡顿不找你找谁?

1.6 一个真实的故事

我记得之前在一个项目中,要处理一个传感器的数据。传感器每 5 毫秒产生一个新数据。工程师直接用了一个 while 循环去轮询状态寄存器。

while (1) {
    if (sensor_status & DATA_READY) {
        process_sensor_data();
    }
}

看起来没问题对吧?但实际跑起来,整个系统的 UI 开始一卡一卡的。为什么?因为这个轮询跑在一个优先级稍高的线程里,它把 UI 线程的 CPU 时间片抢走了。UI 线程每次要渲染画面,都得等这个轮询线程让出 CPU。

后来我们改成了中断触发 + 工作队列的方式,把轮询线程彻底干掉。UI 卡顿瞬间消失。

经验之谈:如果你发现系统中有 L0 卡顿,第一件事就是去检查有没有高频率的轮询。特别是那些看起来“无害”的短轮询。很多时候,卡顿的根源就在这些不起眼的地方。

1.7 本章小结

这一节咱们聊了三个核心概念:

  • 轮询的本质:CPU 懒汉式等待,消耗时间片但不产生价值
  • L0 卡顿的特征:可感知、难复现、随机出现
  • 系统层面的代价:CPU 占用、缓存污染、线程调度延迟

说白了,轮询就像是你站在快递柜前,每隔几秒去按一下“取件”按钮。明明快递还没到,但你就是按。快递员(事件)来的时候,你反而可能因为按得太累没注意到。嗯,这就是轮询的问题所在。

以后你看到 while 循环里有个 if 判断,就要多留个心眼问问自己:这个轮询真的有必要吗?能不能用中断代替?有没有其他更好的方式?这,就是我们从 Polling 到 L0 卡顿排查的起点。