聊到Linux进程优先级,我发现很多人第一个想到的还是“nice值”,觉得调低nice进程就能跑得更快。但真实环境里,把一个进程的优先级调高,它真的就能立刻跑起来吗?不一定。因为决定一个进程“能不能被调度、何时被调度、切走之后多久能切回来”的,是优先级、调度策略、运行队列、抢占时机这四件事在共同起作用。很多线上事故,比如“中间优先级的任务突然跑不动了”“明明不忙CPU使用率却飙高”,根子往往不在优先级数字本身,而在于对这套机制的理解有偏差。
这篇文章我会从调度器内部逻辑讲起,重点拆两件事:优先级体系到底怎么映射的,以及一次进程切换究竟在切换什么。中间会穿插一个真实的“中间优先级任务无法运行”排查案例,把从现象到根因再到修复的完整链路走一遍。适合Linux运维、嵌入式开发、写后台服务被CPU问题折磨过的人,也适合准备操作系统面试的同学拿来当底层原理的复习提纲。
1. Linux优先级体系:一张优先级表,两套调度规则
1.1 先从两个数字说起:nice值和prio值
在Linux里,一个进程可以同时有两个“优先级相关”的数字:一是大家熟知的nice值,范围从-20到19,默认0;二是内核实际用来排序的prio值,也就是task_struct里的prio字段,它的有效范围是0到139。
这两个数字不是一比一映射的,中间隔了一层换算。内核把优先级分成两段:
- 0~99:实时优先级,数值越大优先级越高;
- 100~139:普通进程优先级,数值越小优先级越高,直接从nice值换算。
换算公式很简单:prio = 100 + nice + 20。所以nice为0的普通进程,实际prio是120;nice为-20,prio就是100;nice为19,prio为139。这个看似别扭的设计,其实是为了让调度器在比较优先级时能统一用“数值越小越优先”的规则,实时进程的RT优先级还会再单独映射一次,最终放到同一个数值体系里。
我经常看到有人在网上争论“nice值越低越好还是越高越好”,其实就是没搞明白这层映射。Linux里普通进程的nice值越低,映射出的prio越小,调度优先级越高,但“高”也只是相对普通进程而言,永远高不过任何实时任务。
1.2 实时优先级与普通优先级:100~139这条分界线
内核源码里有个关键宏MAX_RT_PRIO,值是100。凡是prio小于100的进程,都走实时调度类;大于等于100的,全部落入普通调度类。这条线就是两个世界的分水岭。
更具体一点:
| 优先级段 | 调度类 | 对应的用户态设置方式 | 数值越大 |
|---|---|---|---|
| 0~99 | SCHED_FIFO / SCHED_RR | chrt -f / chrt -r | 优先级越高 |
| 100~139 | SCHED_OTHER / SCHED_BATCH / SCHED_IDLE | nice / renice | 优先级越低 |
实时进程和普通进程不会放在同一个运行队列里比较。实时任务挂在rt_rq上,普通进程挂在CFS的cfs_rq上,调度器在pick_next时先看rt队列,再看cfs队列。也就是说,只要有一个实时任务处于可运行状态,普通进程理论上就只能等它让出CPU。这个设计语义很强:实时任务要的是“确定性”,而不是“公平”。
这种硬隔离会带来什么后果?我后面会讲的那个“中间优先级任务跑不动”的案例,就是这条规则引发的。
1.3 优先级高不代表着就能一直跑:调度策略反而更关键
很多刚开始看内核的同学会有一个误解:我只要把进程的nice设成-20,它就能一直占用CPU。实际上不会。对于SCHED_OTHER这类普通进程,即使nice再低,CFS调度器也会通过虚拟运行时间(vruntime)做公平排队,进程跑了一段时间后vruntime变大了,自然会被换下去。
真正能“赖着不走”的只有实时调度策略,而且还得看用的是哪一种:
- SCHED_FIFO:先入先出,只要它不主动让出CPU,同优先级甚至更低优先级的实时任务都抢不走;只有更高优先级的实时任务能打断它。
- SCHED_RR:轮转调度,每个任务在时间片耗尽后必须让位给同优先级的其他任务,但低优先级的依然轮不到。
- SCHED_OTHER:CFS时间共享模型,公平但不保证延迟。
- SCHED_BATCH/SCHED_IDLE:更偏向后台低干扰运行,SCHED_IDLE在普通调度类里几乎是最低优先级。
所以判断一个任务的行为,不能只看priority数字,必须先看它挂在哪个调度类。同一张优先级表里,SCHED_FIFO的50和SCHED_OTHER的50(假设能出现的话)完全是两种命运。
1.4 权重与vruntime:CFS是怎么让普通进程“公平”的
普通进程走的CFS调度器,核心模型不是“时间片轮询”,而是一个理想CPU的加权公平分摊模型。每个普通进程都有一个权重weight,这个权重就是由nice值查表得到的。内核里那张priority_to_weight表特别有意思:nice每降低1级,权重大约增加1.25倍,nice 0是1024,nice -20是88761,nice 19只有15。
调度器维护每个进程的虚拟时间vruntime,它的增长速度跟权重成反比。公式可以简化成:
vruntime += 实际运行时间 * NICE_0_LOAD / 进程权重NICE_0_LOAD就是1024。于是权重越高,vruntime增长越慢;vruntime越小,在红黑树里越靠左,越容易被调度器选中。这就是“高优先级进程看起来跑得更多”的底层原因——不是谁被偏爱,而是它的vruntime涨得慢,总是留在队列左侧。
这个设计也解释了为什么普通进程之间很难出现“饿死”现象:低nice值的进程权重再大,只要它也在运行,vruntime也会涨,只是涨得慢;而CFS会不断挑vruntime最小的进程运行,最终在时间轴上达到比例公平。但注意,这个公平只保证时间占比,不保证响应延迟。如果系统里同时有一个交互型任务和一个CPU密集任务,交互型任务可能每次只跑几毫秒就被切走,而CPU密集任务长时间霸占。这也是后来引入各种优化和组调度的原因。
2. 进程切换的本质:寄存器、内核栈、地址空间三件套
2.1 切换发生在哪里:schedule()与它的调用点
进程切换最终都收敛到同一个函数:schedule()。这个函数做的事概括起来就两句:从当前运行队列里选一个next进程,然后调用__switch_to完成上下文切换。
但真正让工程师头疼的不是schedule本身,而是“什么时候会走到schedule”。内核里的调用点大致分三类:
- 主动让出CPU:进程调用
sleep、wait、mutex_lock之类会阻塞的系统调用时,知道自己要等了,主动调用schedule。 - 被动抢占:中断或时钟tick里设置了一个
TIF_NEED_RESCHED标志,进程在返回用户态时发现这个标志,被迫进入调度。 - 显式yield:进程调用
sched_yield(),表示“我现在自愿让出”。
区分主动和被动很重要。主动让出是进程自己知道“我等的东西还没到”,被切走后状态是TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE;被动抢占是进程还处于TASK_RUNNING,只是被调度器强行拿走CPU,它随时还能回来接着跑。
很多线上性能问题的排查,其实就是在问一个事:到底是谁在频繁调用schedule?如果大量上下文切换来自被动抢占,多半是任务数量超过CPU核数,彼此在抢时间;如果来自主动让出,通常是任务在频繁等待锁、IO或睡眠。
2.2 上下文保存与恢复:一次switch_to做了什么
从概念上说,进程切换就是保存当前进程的硬件上下文,加载下一个进程的硬件上下文。硬件上下文具体是什么?我们以x86_64为例,内核需要保存和恢复的内容包括:
- 通用寄存器:callee-saved寄存器(RBX、RBP、R12~R15)以及RSP、RIP。
- 内核栈指针:每个进程有独立的内核栈(x86_64上通常是16KB),切换时栈指针跟着走。
- FPU/SIMD状态:包括XMM、YMM寄存器,FPU状态默认是懒加载的,用到才切换。
- 线程相关状态:在内核里对应task_struct里的
thread字段,切换的是thread_struct里的sp、ip等信息。
Switch_to宏是一个经典的汇编上下文切换过程。它会先把当前进程的寄存器压栈,然后切换内核栈指针,让RSP指向next进程的内核栈顶,再从next进程的栈里弹出它的寄存器。这个“进栈-切栈-出栈”的动作,一次也就几十条指令,本身并不慢。
真正让切换开销变大的,是下面要说的地址空间。
2.3 地址空间切换为什么最贵:TLB与cache的现实
如果两个进程属于同一个线程组,它们共享mm_struct,切换时不需要改动CR3寄存器,开销很小。但如果切换的是两个完全独立的进程,内核需要加载新的页表基址——在x86_64上就是改写CR3。CR3一变,TLB(页表缓存)里与旧地址空间相关的条目就全部失效。
失效意味着下一次访问内存时,MMU需要重新走页表遍历,这个过程是纯硬件开销,虽然快,但很频繁。更麻烦的是cache:用户态进程的代码和数据可能分布在完全不同的物理地址,切换后L1/L2里旧进程的热数据帮不上忙,新进程要重新把热数据拉进cache,这段时间的指令流水线效率大幅下降。
有人可能问:系统每次调度就那么几个微秒,cache损失真能影响吞吐吗?在极端场景下能。比如一个高频率切换的负载均衡服务,如果每秒切换上万次,每一次切换后都要经历一轮cache冷启动,CPU的有效执行效率可能下降20%以上。这也是为什么现代内核在调度时特别看重“缓存亲和性”——同一个进程尽量在原CPU上跑,甚至引入了wakup机制里的wake_affine逻辑,优先唤醒在同一个核上。
2.4 切换开销的量化:一次上下文切换到底损失多少
直接做实验,用context_switch基准测试或者lmbench的lat_ctx工具,结果因CPU型号和内核版本差异很大。以常见的x86服务器CPU为例,一次纯用户态上下文切换大约耗时1~3微秒;如果包含地址空间切换,可能到5微秒以上;如果是实时优先级抢占,还要额外加上调度决策时间和唤醒延迟。
但更值得关注的不是单次切换耗时,而是切换频率乘出来的总量。我习惯用pidstat -w看每秒上下文切换次数,或者用vmstat 1看cs列。一个简单的估算思路:
单核每秒可以执行约2~3百万次切换(按1~2微秒每次算),如果每秒切换总次数超过50万,CPU的“有效算力”就已经开始被调度本身吃掉了。
线上遇到“CPU不忙但任务很卡”的情况,经常是切得太频繁,每个任务都在排队,谁也没跑多久就被换下去。这时候调优先级之前,不如先想办法降低切换频率:增加轮询间隔、减少锁竞争、让线程数收敛到核数。
3. 优先级如何决定“什么时候切换”:抢占时机分析
3.1 抢占不是随时都能发生:need_resched机制
这里要先纠正一个印象:不是“高优先级进程来了,低优先级进程马上被踢走”。内核的抢占是协作式的,即便设置了抢占标志,也得等到一个“安全点”才能真正执行调度。这个标志就是TIF_NEED_RESCHED,它挂在进程的thread_info里。
触发链大致如下:
- 高优先级进程被唤醒,唤醒路径会调用
check_preempt_curr,看看当前运行的进程是否为更低优先级; - 如果是,就通过
resched_curr给当前进程设置TIF_NEED_RESCHED; - 当前进程在从内核态返回用户态时检查这个标志,发现置位,才进入schedule。
这意味着切换时机取决于当前进程处在什么上下文。如果它正在用户态跑,下一次系统调用返回或中断返回时就能被抢占,通常很快;如果它正在内核态执行某些关键代码,可能要等到内核完成临界区操作、释放自旋锁、或到达内核里允许抢占的检查点。
3.2 用户态抢占与内核态抢占的差异
Linux在很长时间里只有“用户态抢占”,即内核代码不可被随便打断。这种情况的典型代表是老式的自旋锁保护下的临界区,一旦占着CPU,其他进程只能干等。后来引入了CONFIG_PREEMPT系列配置,才支持在更多内核路径上做抢占:
- CONFIG_PREEMPT_NONE:服务器取向,内核态几乎不可抢占,吞吐优先,延迟大。
- CONFIG_PREEMPT_VOLUNTARY:在某些长时间执行的路径上主动检查need_resched,延迟稍好。
- CONFIG_PREEMPT:桌面/嵌入式取向,内核大部分代码可抢占,响应快但吞吐略降。
- CONFIG_PREEMPT_RT:实时内核补丁,几乎全内核可抢占,用于对延迟极其敏感的场景。
对于普通服务器,默认一般是PREEMPT_NONE或VOLUNTARY。这种配置下哪怕你的进程是SCHED_FIFO的高优先级实时任务,也敌不过当前进程恰好在内核持锁临界区——它只能等锁释放、等内核交还。这个“等”的过程,实际上会让实时任务的调度延迟从个位数微秒膨胀到几十甚至上百微秒。
3.3 唤醒抢占与调度延迟:优先级能多快地起作用
判断一个任务“切得快不快”,有两个指标值得关注:
- 唤醒延迟(wakeup latency):从任务被唤醒到真正得到CPU的时间;
- 调度延迟(scheduling latency):从任务进入TASK_RUNNING到被选中运行的时间。
优先级在这里的作用是决定“要不要抢占当前进程”。对实时任务,只要唤醒者是更高优先级,基本能立即抢占(除非碰上不可抢占内核区);对普通进程,CFS不会因为“有任务唤醒”就无条件抢占,它要看被唤醒任务的vruntime是否比当前curr的vruntime小得多,或者通过sysctl_sched_wakeup_granularity_ns控制的最小抢占粒度。
换句话说,普通优先级只在“环境公平”的框架里影响调度次序,实时优先级则是直接按数值抢人。如果你想验证,可以做个简单实验:起两个CPU密集型进程,一个用chrt -f设定实时优先级,另一个普通进程,观察那个普通进程还能不能定期拿到CPU。在单核环境里,普通进程大概率会长时间得不到调度。
3.4 不同调度策略的切换行为对比
把几种策略放在一张表里看会更清楚:
| 调度策略 | 选谁运行 | 何时被切走 | 对延迟的语义 |
|---|---|---|---|
| SCHED_FIFO | 同优先级队列最前 | 主动让出或被更高优先级打断 | 无时间片约束,可能霸占CPU |
| SCHED_RR | 同优先级轮转 | 时间片耗尽 | 时间片默认100ms,可配置 |
| SCHED_OTHER | vruntime最小 | vruntime被拉大或被抢占 | 公平优先,延迟不保证 |
| SCHED_BATCH | 类似CFS | 更容易睡大觉 | 适合后台批处理 |
| SCHED_IDLE | 空闲才跑 | 随时被更高优先级抢 | 最低优先级 |
我记得有一次做嵌入式项目,两个任务都用SCHED_FIFO,一个优先级40,一个优先级30。按理说40比30高,但它们的实际执行间隔差异被人为放大到离谱。根因是优先级40那个任务里写了一个高频定时器,每次触发都去操作同一个串口,而串口驱动里的锁又会被低优先级任务长时间持有,导致高优先级任务频繁被阻塞,反而把低优先级的执行节奏全打乱了。所以单纯看优先级数字,真的会误判。
4. 一个真实案例:中间优先级的任务为什么“消失”了
4.1 现象与初步排查
某次线上的采集服务出现了诡异症状:系统里有三个关键任务,按优先级预期应该是 A > B > C,A是实时采集,B是中间层聚合,C是普通上报任务。结果运行一段时间后,B几乎不输出数据了,可A和C都看起来正常。更诡异的是,CPU整体占用不到30%,完全不像资源耗尽。
我们用ps先看B的状态:
ps -eo pid,comm,stat,pri,rtprio,cls输出里B显示的是R状态,rtprio=30,cls=FF(SCHED_FIFO),也就是说它明明处于可运行状态,优先级也不算低,为什么得不到CPU?
接着看全局状态,top按P排序,A和C都占着CPU,B排在很后面,CPU时间基本是0。这时有个直觉:B是被“饿”了,但谁在饿它?
4.2 完整排查链路:从top到/proc/sched一步一步来
我们用top确认了B的CPU时间几乎没有增长,然后用一个更装深度的命令看调度器内部:
cat /proc/B_PID/sched重点关注这几个字段:
se.statistics.sum_exec_runtime:累计运行时间,如果几乎不涨,说明B根本没被执行;se.vruntime:CFS的虚拟时间,对实时任务意义不大;policy和rt_priority:确认调度策略。
再看整个系统的RT队列情况,可以临时打开调度器调试信息:
cat /proc/sched_debug里面能看到每个CPU上rt_rq的active列表。问题一下就清楚了:CPU0上A的优先级是50,B的优先级是30,A一直挂在rt队列头部,B排在它后面。而A作为一个SCHED_FIFO任务,只要不主动放弃CPU,同队列里的所有低优先级任务永远没机会。
4.3 根因定位:SCHED_FIFO高优先级进程独占CPU
为什么A会一直占用CPU?查A的业务日志发现,A内部有个状态机,如果对端设备不在线,它会持续轮询重试,每次查询之间的间隙本来有一段sleep,但因为代码里某个异常分支提前返回,sleep被跳过了,直接进入下一轮查询。结果A就从“采集-睡眠-采集”变成了“采集-采集-采集”,始终处于TASK_RUNNING。
按照Linux调度规则,A是CPU0上的最高优先级RT任务,它不停顿,B和C在CPU0上永远排不上号。C之所以还能输出,是因为C后来被负载均衡迁移到了CPU1上,CPU1没有高优先级RT任务霸占,所以C还能正常跑;而B由于CPU亲和性设置或迁移条件不满足,一直留在CPU0上陪着A“陪跑”。
你可以用taskset -pc PID确认这一点:A和B都绑在CPU0上,C在CPU1上。
4.4 修复方案与验证
修复分三步走:
- 修逻辑漏洞:让A在异常分支里也能正确睡眠,恢复“采集-睡眠-采集”的节奏;
- 隔离关键任务:用
taskset把A固定在CPU2,B固定在CPU3,让两个实时任务不再互相干扰; - 限制RT占用的总带宽:用cgroup的cpu控制器限制实时任务可使用比例,防止极端情况下A再次爆发。
关键操作示例:
# 把A固定在CPU2 taskset -pc 2 A_PID # 设置cgroup实时带宽限制,比如period=1s,rt_runtime=500ms mkdir /sys/fs/cgroup/cpu/rt_limit echo "500000" > /sys/fs/cgroup/cpu/rt_limit/cpu.rt_runtime_us echo "1000000" > /sys/fs/cgroup/cpu/rt_limit/cpu.rt_period_us echo A_PID > /sys/fs/cgroup/cpu/rt_limit/tasks验证过程很简单:再次观察B的CPU时间曲线,几分钟后开始稳定增长;/proc/B_PID/sched里的sum_exec_runtime不再停滞。系统的任务执行节奏恢复正常。
4.5 从案例反推:优先级设计要避开的几个坑
这个案例里暴露出来的问题,在真实项目里并不罕见,总结下来有四个坑:
- SCHED_FIFO任务里绝对不能出现长时间忙等。哪怕你只是在一个异常分支里少了行sleep,也足以让更低优先级的任务彻底瘫痪。实时优先级的优先力太强了,强到“不给普通进程活路”。
- 多核环境下要注意CPU亲和性。明明有四个核,两个高优先级任务挤在一个核上互相竞争,另一个核空着,这种负载不均现象非常常见。
- 实时任务要留抢占出口。如果无法彻底避免忙等,也要在循环里加
sched_yield()或周期性地sleep,让更低优先级的任务有喘息机会。 - 用cgroup限制RT带宽作为保险。内核提供
cpu.rt_runtime_us这个参数就是干这个用的,宁可设置一个保守的上限,也别让实时任务在异常时把整机拖垮。
5. 优先级反转与继承:一个比“优先级不够高”更隐蔽的问题
5.1 经典三进程模型演示
如果说上一节的案例是“太高优先级饿死别人”,优先级反转则是另一种更隐蔽的灾难。它有一套经典的模型:
- 进程L:低优先级,持有某把锁;
- 进程M:中间优先级,准备抢CPU;
- 进程H:高优先级,等待L持有的那把锁。
逻辑上的正常流程很清晰:H等待锁,L应该尽快运行并释放锁,好让H拿到锁继续跑。但真实调度里有个意外:L刚准备睡醒去释放锁,M这个中间优先级任务突然就绪,抢在L之前占住了CPU。按照优先级调度规则,M比L高,L根本抢不过M,只能等M睡着或时间片耗尽才能继续。结果H这个最高优先级任务,反而要等L跑完,而L要等M跑完。中间的M成了实际的“最高优先级”,整个系统的调度逻辑被倒挂。
5.2 RT mutex与优先级继承
Linux针对这个问题设计了优先级继承机制,最典型的实现就是RT mutex。它的核心策略是:当高优先级任务H等待一把锁时,锁持有者L的优先级被临时提升到H的优先级。这样M再想抢占L就占不到了,因为此时L的有效优先级已经高于M。L能立刻执行、释放锁、把优先级恢复原值,然后H获得锁继续运行。
在用户态,这种能力体现在pthread_mutexattr_setprotocol里,可以设置PTHREAD_PRIO_INHERIT。很多做实时应用的同学只关注线程优先级,却忽略了锁的属性,导致明明用了RT线程,延迟还是高得离谱。我见过一个典型错误:整个软件里所有线程都是SCHED_FIFO,但互斥锁全是默认的普通futex锁,一旦发生锁竞争,高优先级线程照样被低优先级线程拖住,完全体现不出实时的效果。
5.3 实测中的判断技巧
怎么快速判断系统里是否发生了优先级反转?可以从两个方向入手:
- 观察锁等待时间:用
perf lock或lockdep抓锁竞争的热点,看高优先级任务的等待时间是否异常拉长; - 观察调度延迟:用
cyclictest测量实时任务的调度延迟,如果延迟曲线出现周期性尖峰,大概率是某个低优先级任务持有锁后又被中间任务抢占。
我在调优一个多线程实时系统时,用过的有效组合是:给所有实时线程设置SCHED_FIFO+ 给所有互斥锁设置PTHREAD_PRIO_INHERIT+ 禁用不必要的CPU频率缩放。做完这三步,最高延迟从几百微秒降到个位数微秒。优先级反转这个问题,在实际系统里远比教科书上讲的频率高,尤其是线程多了之后。
6. 实践中的优先级设计建议与调优工具
6.1 该用哪些命令查看和修改优先级
既然聊到实践,先把常用命令列一下,都是日常排查用得上的。
# 启动时设定nice值 nice -n -5 ./your_program # 修改已运行进程的nice值 renice -n -5 -p PID # 查看进程的优先级和调度策略 ps -eo pid,comm,pri,nice,rtprio,cls,stat # 设置实时调度策略:SCHED_FIFO,优先级50 chrt -f -p 50 PID # 设置SCHED_RR chrt -r -p 50 PID # 查看指定进程的调度参数 chrt -p PID其中rtprio列显示的是实时优先级,普通进程显示-;cls列显示调度类,TS代表SCHED_OTHER,FF代表SCHED_FIFO,RR代表SCHED_RR。看这两列基本就能判断一个进程处在哪个优先级世界。
6.2 应用层调度参数的设计原则
给应用设计优先级时,我一般遵循几个简单原则:
- 只有确实需要确定性的任务才用RT策略,比如音频播放、运动控制、高频交易撮合、采集线程。别把整组线程都设为实时,否则调度器没有普通进程的发挥空间。
- RT优先级之间留出合理间距。不要全都设成99,至少要留两三个等级差,才能让抢占关系清晰。如果两个任务有依赖关系,比如生产者需要消费者消费后才继续,那生产者的优先级不应该高于消费者,否则容易互相卡死。
- 普通进程之间,用nice值调权重而非调响应。CFS本质是权重公平模型,想提高某任务的CPU占用比例,可以适当降低nice值,但别指望降低nice能改善交互延迟。
- 配合cgroup做隔离。给关键业务一个单独的cpu cgroup,设置好权重和带宽上限,能有效防止一个失控的叶子任务拖垮整个宿主。
6.3 与cgroup、affinity组合使用的思路
最后说一个组合使用案例。一个容器化的微服务经常出现CPU毛刺,排查发现:容器内一个实时采集线程的SCHED_FIFO优先级很高,但容器宿主上其他容器的普通线程被它压制。我们做了两件事:
第一,把采集线程用taskset绑定到固定CPU,不会到处迁移打乱其他容器的cache;第二,在cgroup里给该容器设置cpu.rt_runtime_us=100000、cpu.rt_period_us=1000000,限制实时类任务最多占用10%的CPU带宽。
这样即便采集线程异常忙等,它对宿主整体CPU的影响也被限制在可控范围内。调度机制的很多参数是全局生效的,但在容器和虚拟机时代,真正管好资源边界的是cgroup,而不是单个进程的nice值。
我自己的习惯是:写任何CPU密集型或实时相关的守护进程,都先想清楚三件事——这个进程应该用哪个调度类,是否会跟其他进程抢锁,最坏情况下它会不会独占CPU。把这三个问题提前写在代码注释里,比线上出了问题再翻文档要有用得多。