想象你是一家餐馆的老板:生意火爆,你一口气雇了四位帮厨——这就是多线程。可这家后厨有个奇怪的店规:全店只有一个炉灶,用火权一次只能给一个人。CPU 计算就像炒菜,必须占着炉灶;而等水烧开、等食材送到,就像网络请求与文件读写,完全不需要炉灶。于是你会发现:四个厨师同时抢着炒菜,锅只有一口,时间一点没省;可四个人各自等自己的水烧开,效率立刻翻了倍。炉灶是 GIL,帮厨是线程,串起这件事的,还有抢菜刀时要用的锁、传菜窗口的条件变量。这篇就把 Python 多线程与 GIL 的那点事,用后厨里的烟火气讲明白。

岗位关联

吃透 GIL 与线程模型,是写出高并发 IO 程序、在面试中讲清 Python 并发本质的必修课,也是迈向异步框架的第一块垫脚石。

目录
  1. 可视化演示
  2. 线程的创建
  3. 互斥锁同步
  4. 条件变量通信
  5. GIL运行原理
  6. GIL现实影响
  7. 线程安全策略
  8. 线程还是进程
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Python多线程在IO密集和CPU密集场景下的执行差异,GIL如何在线程间切换,以及为何CPU密集任务无法真正并行。

本节配有交互动效(进入视口后自动加载,约 3KB)。

IO密集模式 IO等待时释放GIL,多线程并发执行 线程在IO阻塞时让出执行权 并行度 ≈ 线程数

线程的创建

一家小餐馆的老板发现生意火爆,自己一个人又洗又切又炒根本忙不过来,于是贴出招聘启事,请了几位帮厨。每位帮厨只负责自己手里的订单,两人洗菜、两人切菜,互不干扰,仿佛饭馆凭空多了几个分身。Python 里的线程(Thread)就是这个分身:进程是那家餐馆,线程是餐馆里各自忙碌的帮厨,它们共享同一份食材、同一本账本。

创建线程有两种主流姿势,和招聘的两种方式很像。第一种是“招科班厨师”:继承 threading.Thread 类,把要做的事写进 run 方法,适合逻辑复杂、需要长期复用的任务;第二种是“临时外包”:直接传一个函数给 Thread(target=worker, args=(...)),函数跑完外包结束,简洁轻快。

创建后要调用 start() 让线程真正开工,直接调用 run() 只是在当前线程里“假装干一遍”。主线程可以用 join() 等大家收工;还可以通过 daemon=True 把线程设为守护线程,它就像“主厨下班就散伙的兼职”,主进程一退出,守护线程自动消失。线程从新建、就绪、运行、阻塞到结束,就是它的生命周期。

回到后厨:start() 是喊一声“上工”,join() 是老板在大门口等着最慢的帮厨收工。真正让四个帮厨抢起活来又热闹又出乱子的,是下一节那把共享的钥匙。

Tjoin=max⁡(t1,t2,…,tn)\displaystyle T_{join} = \max\left(t_1, t_2, \dots, t_n\right)
老板在门口等所有人收工,必须等到最慢的那位帮厨。
  1. t_i 表示单个线程耗时每个帮厨完成自己那份订单需要的时间,各不相同。
    tit_i
  2. max 取最大值主线程用 join 等待所有线程结束时,必须等到最慢的那一个。
    max⁡(t1,t2,…,tn)\max(t_1, t_2, \dots, t_n)
  3. 整体耗时在 IO 并发下,总耗时约等于最慢线程的耗时,而不是把所有耗时累加。
    Tjoin=max⁡(t1,t2,…,tn)T_{join} = \max(t_1, t_2, \dots, t_n)

互斥锁同步

帮厨多了,问题就来了:厨房里只有一把削皮刀,张三刚拿起来削土豆,李四伸手就抢,两人一扯,土豆滚了一地。共享资源需要“先到先用”的规矩,这把“钥匙”就是锁(Lock)。Python 中 threading.Lock 一次只允许一个线程进入受保护的区域(临界区),其他人只能在门口排队。

使用方式很机械:进门前 acquire(),出门必须 release(),更优雅的是 with lock: 的写法,即使中途抛异常也会自动还钥匙。如果同一线程要“嵌套进门”,普通 Lock 会把自己卡死——你拿着钥匙进屋,又想在屋里再拿一次钥匙,可钥匙在你手里,于是永远等下去。这时要用可重入锁 RLock,它记录重入次数,进几次出几次。

死锁是最经典的翻车现场:厨师 A 拿着削皮刀等炒锅,厨师 B 端着炒锅等削皮刀,两人对视到天黑。死锁需要四个条件:互斥、持有并等待、不可剥夺、循环等待;打破任何一个都能化解。

锁是后厨的“规定动作”,但规定多了也会拖慢出菜速度。临界区越短越好,就像用钥匙开柜子拿完就走,别站在柜门前数鸡蛋。

Ttotal=∑i=1ntcrit(i)+tother\displaystyle T_{total} = \sum_{i=1}^{n} t_{crit}^{(i)} + t_{other}
锁让临界区只能排队进入,这部分时间会被串行累加。
  1. t_crit 临界区耗时持锁执行的那段“只许一个人进”的时间。
    tcrit(i)t_{crit}^{(i)}
  2. Σ 累加锁是互斥的,n 个线程的临界区只能排队串行,总时间是把它们全部相加。
    ∑i=1ntcrit(i)\sum_{i=1}^{n} t_{crit}^{(i)}
  3. t_other 并行部分不碰共享资源的活可以同时干,这部分不参与累加。
    tothert_{other}

条件变量通信

传菜窗口是后厨最聪明的发明。厨师做好菜放在窗口,按一下铃;服务员听到铃响才来端走;窗口空空时,服务员就在旁边打盹。Python 的条件变量(Condition)就是这口铃,它解决的是“什么时候可以干活、什么时候该睡觉”的问题。

Condition 必须与一把锁搭配。厨师(生产者)进入临界区后调用 notify() 摇铃;服务员(消费者)发现窗口空了,就调用 wait() 自动释放锁并挂起睡觉,等铃响后再醒来重新抢锁。注意:wait() 醒来后要再用 while 检查条件,防止“假醒”后白跑一趟。

如果你不想自己摇铃,直接用 queue.Queue 就好。它是官方封装好的“传菜窗口”,内部自带锁和条件变量,支持 maxsize 限流、put/get 阻塞等待,生产者和消费者彻底解耦。这是绝大多数生产环境的首选。

传菜窗口把“做菜”和“上菜”彻底分开:厨师不用等服务员,服务员不用催厨师。理解了这口铃,你就掌握了线程间协作的节奏感。

Q(t)=P(t)−C(t)\displaystyle Q(t) = P(t) - C(t)
队列里积压的任务数,等于生产总数减去消费总数。
  1. P(t) 累计生产量厨师总共出锅并放进窗口的菜数。
    P(t)P(t)
  2. C(t) 累计消费量服务员总共端走的菜数。
    C(t)C(t)
  3. Q(t) 队列积压窗口里正在等传菜的数量,条件变量在 Q=0 时阻塞消费者,在到达容量上限时阻塞生产者。
    Q(t)=P(t)−C(t)Q(t) = P(t) - C(t)

GIL运行原理

现在揭晓后厨最大的秘密:这家店虽然雇了八个帮厨,却只有一个炉灶,而且老板规定“用火权一次只给一个人”。这把全局的“用火权”就是 GIL(Global Interpreter Lock,全局解释器锁)。

GIL 不是 Python 语言的规定,而是主流解释器 CPython 的实现细节。它的存在是为了保护内存安全:Python 内部用引用计数管理对象,如果两个线程同时修改同一个对象的引用计数,计数就会错乱,甚至导致内存泄漏或程序崩溃。GIL 让“改计数”这件事一次只有一个人能做,相当于老板在炉灶边立了块牌子:安全第一。

代价是:同一时刻,整个进程只有一个线程在执行 Python 字节码,多核 CPU 也派不上用场。但 GIL 并非永远霸占,解释器每执行一个时间片就检查一次要不要把炉子让出去,遇到 IO 阻塞则会立刻释放。

炉灶规矩保护了后厨的安全,却也限制了并行炒菜的可能。老板心里清楚:想真正快起来,得看这口锅到底被用来炒菜还是等水。

Tslice=5×10−3 s\displaystyle T_{slice} = 5 \times 10^{-3}\,\text{s}
默认约 5 毫秒,解释器就会检查一次“有没有人在等炉子”。
  1. T_slice 切换检查间隔解释器每隔这么多时间就检查一次是否切换 GIL 持有者。
    TsliceT_{slice}
  2. 5 × 10⁻³ 秒默认约 5 毫秒,可用 sys.setswitchinterval 调整。
    5×10−35 \times 10^{-3}
  3. 时间片越短切换越频繁响应更快,但线程来回换手的开销也更大。
    s(秒)\text{s(秒)}

GIL现实影响

弄懂了炉灶规则,就能解释一个反常现象:四个帮厨同时抢着炒菜,锅只有一口,炒好的菜和一个人炒的一模一样多,甚至更少——因为大家还得互相让位。这就是 CPU 密集任务:多线程加速比约等于 1,开了等于没开。

可如果四个帮厨都在等自己的水烧开呢?等水不需要炉灶,这时谁占着用火权都无所谓,四个人可以同时等。这就是 IO 密集任务:线程在等待网络响应、磁盘读写时主动释放 GIL,其他线程趁机执行,并发效果立刻显现。爬虫、Web 服务、文件处理全是这类场景。

Amdahl 定律能帮我们看清天花板:任务的加速比由可并行部分和串行瓶颈共同决定。在 GIL 之下,CPU 计算部分相当于被强制串行化,于是加速比趋近 1;而 IO 等待既不算并行计算,也不需要占着 GIL,反而躲开了它的约束。

所以判断该不该开多线程,先问一句:你在炒菜,还是在等水开?炒菜请找进程,等水请找线程。

S=1(1−P)+P/N\displaystyle S = \frac{1}{(1 - P) + P / N}
串行瓶颈决定性能天花板,GIL 正是 CPU 密集任务的“串行瓶颈”。
  1. P 可并行比例任务中能够同时执行的部分占多少。
    PP
  2. N 执行单元数核心数或进程数,线程在 GIL 下等效于 1。
    NN
  3. (1-P) 串行瓶颈无法并行的部分,是加速比的天花板。
    1−P1 - P
  4. GIL 场景代入CPU 计算被串行化,S 约等于 1;IO 等待不算并行计算,反而绕开了 GIL。
    S=1(1−P)+P/NS = \frac{1}{(1 - P) + P / N}

线程安全策略

帮厨们除了抢刀抢锅,还会抢“台账”。假设冰箱里的鸡蛋数记在账本上,张三看了一眼是 10 个,拿了一个想改成 9;还没落笔,李四也看了一眼,看到的还是 10,也拿了一个改成 9。结果账本记 9,实际只剩 8 个。这就是竞态条件(race condition)。

GIL 能保护解释器内部的引用计数,却保护不了你的业务数据。x += 1 看似一步,实际是“读旧值、加一、写回”三步,三步之间线程随时可能被切换。GIL 是炉灶规矩,不是账本的保险柜。

让数据线程安全有三招:一是依赖原子操作(单条字节码级别,如简单的赋值),二是在复合操作外加锁,三是优先使用官方安全容器,比如 queue.Queue、threading.local,把风险关进笼子里。

账本的教训告诉我们:共享数据是后厨最脆弱的地方。要么用锁看得死死的,要么干脆用带锁的传菜窗口,别让两个帮厨同时动笔。

x=x+1  ⇒  read(x)→x+1→write(x)\displaystyle x = x + 1 \;\Rightarrow\; \mathrm{read}(x) \rightarrow x+1 \rightarrow \mathrm{write}(x)
看似一步的自增,实际是读、加、写三步,三步之间随时可能被切换。
  1. read(x) 读取旧值从内存把 x 当前值拿出来。
    read(x)\mathrm{read}(x)
  2. x+1 计算新值在寄存器里完成加一。
    x+1x + 1
  3. write(x) 写回把新值写回内存。
    write(x)\mathrm{write}(x)
  4. 三步可被打断GIL 只保护单条字节码,读和写之间线程可能被切换,导致更新丢失,所以必须加锁。
    x=x+1x = x + 1

线程还是进程

如果这家餐馆真到了要同时炒四桌菜的地步,老板的终极方案是开四家分店:每家一个独立厨房,各有一口炉灶,真正并行炒菜。Python 里这叫进程(Process),multiprocessing 模块就是“开分店”。

分店多了,麻烦也多了:各店有自己的食材库存,不能直接共享锅碗瓢盆;要协作就得走“跨店传送”——进程间通信(IPC),比如队列、管道、共享内存,成本远高于线程间的传菜窗口。线程切换轻、通信快,但受 GIL 约束;进程真并行,但创建和通信都重。

所以选型口诀:IO 密集任务(爬虫、Web 服务、读写文件)——线程开销小、通信快,优先线程;CPU 密集任务(图像处理、数值计算、模型推理)——需要真并行,优先进程。线程池规模也有经验公式。

后厨的故事到这里讲完了:线程是帮厨,GIL 是炉灶,锁是钥匙,队列是传菜窗,进程是分店。理解了这些角色,你就能在并发编程里从容点单。

N=Ncpu×(1+TwaitTcompute)\displaystyle N = N_{cpu} \times \left(1 + \frac{T_{wait}}{T_{compute}}\right)
IO 占比越高,越值得多开线程去重叠等待,线程池规模就越大。
  1. N_cpu CPU 核心数服务器上真正能并行干活的“炉灶数”。
    NcpuN_{cpu}
  2. T_wait 等待时间每次任务等网络或磁盘的时间。
    TwaitT_{wait}
  3. T_compute 计算时间真正占 CPU 的时间。
    TcomputeT_{compute}
  4. 比例越大线程越多IO 占比越高,越值得多开线程去重叠等待,线程池规模就越大。
    TwaitTcompute\frac{T_{wait}}{T_{compute}}

AI 应用场景

高并发网络爬虫

阶段:数据采集与预处理

解决的问题:传统串行爬虫发一个请求就要干等响应,1000 个页面要等 1000 轮网络延迟。用多线程后,每个线程负责一批 URL,网络等待时主动释放 GIL,其他线程继续发请求,等待时间被大量重叠,吞吐提升数倍。

具体案例:某 AI 公司的知识库需要从 10 个新闻站点抓取语料。爬虫工程师用 ThreadPoolExecutor 开 32 个线程并发抓取,配合 queue.Queue 做 URL 调度与去重,抓取耗时从 2 小时降到 8 分钟。

日志上报与指标采集管道

阶段:模型服务可观测性与运维

解决的问题:推理服务每秒产生大量日志,直接写磁盘会互相覆盖。用生产者-消费者模型:业务线程把日志 put 进 Queue,后台消费者线程批量 flush 到文件或监控系统;Queue 内部自带锁,保证不丢不乱。

具体案例:某大模型推理平台用 4 个消费者线程从队列批量上报 token 耗时、GPU 利用率等指标,写盘冲突归零,监控面板延迟降到亚秒级。

LLM 推理服务并发请求

阶段:模型部署与推理优化

解决的问题:大模型服务每秒接入大量用户请求,单线程处理会让用户排队到天荒地老。服务端用线程池承接请求,网络传输与 token 生成间隙释放 GIL,多请求并发处理,显著提升吞吐。

具体案例:某对话机器人服务用 32 线程处理并发请求,每个请求独立等待模型返回;高峰期同时在线 200 个会话,接口 P95 延迟仍保持在 1.5 秒内。

Python 演示

python60 行
import threadingimport timefrom queue import Queue def cpu_puzzle(n):    total = 0    for i in range(n):        total += i * i    return total def io_puzzle(times, gap=0.1):    for _ in range(times):        time.sleep(gap) def timeit(fn, *args):    t0 = time.time()    fn(*args)    return time.time() - t0 def run_threads(fn, args, workers=4):    ts = [threading.Thread(target=fn, args=args) for _ in range(workers)]    t0 = time.time()    for t in ts:        t.start()    for t in ts:        t.join()    return time.time() - t0 def producer(queue, total):    for i in range(total):        queue.put(i)    queue.put(None) def consumer(queue):    while True:        item = queue.get()        if item is None:            break        _ = item * item if __name__ == "__main__":    n = 2_000_000    single = timeit(cpu_puzzle, n)    multi = run_threads(cpu_puzzle, (n,))    print(f"CPU密集: 单线程 {single:.2f}s, 4线程 {multi:.2f}s, 加速比 {single / multi:.2f}")     single = timeit(io_puzzle, 20)    multi = run_threads(io_puzzle, (20,))    print(f"IO密集: 单线程 {single:.2f}s, 4线程 {multi:.2f}s, 加速比 {single / multi:.2f}")     q = Queue(maxsize=8)    producers = [threading.Thread(target=producer, args=(q, 2000)) for _ in range(2)]    consumers = [threading.Thread(target=consumer, args=(q,)) for _ in range(2)]    for t in producers + consumers:        t.start()    for t in producers:        t.join()    for t in consumers:        t.join()    print("生产者-消费者演示完成: Queue 内部锁与条件变量工作正常")
目的:用真实计时对比 CPU 密集与 IO 密集任务在多线程下的加速差异,并演示基于 Queue 的生产者-消费者模式。
  1. import threading引入“招帮厨”的并发模块,所有线程能力都从这里来。
  2. threading.Thread(target=fn, args=args)每个线程绑一个任务函数,相当于给帮厨派单。
  3. t.start()真正“上工”,创建并启动线程;只调 run() 不会开新线程。
  4. t.join()老板在门口等所有人收工,主线程阻塞直到子线程结束。
  5. time.sleep(gap)模拟 IO 等待,此时不占炉灶,GIL 被释放给其他线程。
  6. Queue(maxsize=8)限流 8 个任务的传菜窗口,内部自带锁和条件变量。
  7. q.put(item)窗口没满就放菜,满了自动阻塞等待。
  8. item = q.get()窗口有菜就端走,空了自动阻塞等待。
  9. q.put(None)“毒丸”哨兵,通知消费者收工,优雅退出循环。

常见误区

误区听说 Python 有 GIL,多线程根本不能并行,所以永远别用多线程,用了也白用。

正解GIL 只卡 CPU 计算。对网络请求、文件读写这类 IO 密集任务,线程在等待时主动释放 GIL,多线程能把几百个请求的等待时间重叠起来,吞吐成倍提升。就像等水开的四个帮厨可以同时等,互不耽误。

为何常见:很多人只看到 CPU 密集任务的加速失败,就把 GIL 一棍子打死,忽略了 IO 场景这个真正的主战场。

误区既然 GIL 保证同一时刻只有一个线程执行字节码,那共享数据天然安全,不用加锁。

正解GIL 只保护解释器内部(比如引用计数),不保护你的业务数据。x += 1 会拆成读、加、写三步,三步之间线程可能被切换,照样丢更新。GIL 是“炉灶规矩”,不是“台账的保险柜”。

为何常见:GIL 的“一次只执行一个线程”听起来很像互斥,容易让人误以为解释器天然给所有数据上了锁。

误区反正要并发,线程池开 200 个、500 个,总比 4 个快。

正解线程有创建、销毁和上下文切换成本;线程太多还会加剧 GIL 竞争和锁等待,性能反而下降。经验做法是按公式 N = N_cpu × (1 + Twait/Tcompute) 估算规模。

为何常见:直觉上人多好办事,忽略了调度开销这条“排队成本”,就像后厨塞进 50 个人只会互相撞胳膊。

误区`wait()` 醒来后条件一定满足了,用 `if` 检查一下就行,不需要 `while`。

正解官方强烈建议用 while 循环重新检查条件。线程可能“虚假唤醒”(spurious wakeup),或被其他 notify 误唤醒,醒来后条件未必满足。用 while 是标准写法:醒了发现窗口还空,接着睡。

为何常见:很多入门例子用 if 也能跑通,掩盖了并发下的偶发风险,直到线上偶现 bug 才翻车。

误区Lock 和 RLock 都叫锁,功能差不多,闭眼用就行。

正解Lock 在同一线程内重复 acquire 会把自己卡死(不可重入);RLock 允许同线程重入,适合递归或嵌套调用的场景。但 RLock 解决不了多线程之间的循环等待死锁,别指望它包治百病。

为何常见:单线程测试很难暴露重入死锁,只有写递归锁保护的代码时才突然翻车。

面试题

Q1. 在 Python 中创建了 threading.Thread 对象后,调用哪个方法才会真正开启新线程?

单选题 · easy

  • A.run()
  • B.start()← 正确答案
  • C.join()
  • D.acquire()

答案:B。start() 才会在解释器里创建一个真正的操作系统线程并调度执行;run() 只是在本线程里直接调用函数体,相当于“假装开新线程”。就像喊“上工”才开始干活,而 run() 只是自己把活干了。

Q2. 关于 GIL 的说法,哪一项是正确的?

单选题 · easy

  • A.它是 Python 语言规范强制要求的
  • B.所有 Python 解释器都必须实现它
  • C.它是 CPython 解释器里保护内存管理的一把全局锁← 正确答案
  • D.它锁定了 CPU 硬件核心

答案:C。GIL 只是 CPython 的实现细节,Jython、PyPy 等解释器可以没有。它锁的是“解释器进程内一次只能有一个线程执行字节码”,不是锁硬件。相当于“唯一炉灶”是这家店的店规,不是所有饭店的规矩。

Q3. IO 密集任务用多线程效果明显,最主要的原因是?

单选题 · easy

  • A.多个线程真的在多个 CPU 核心上并行计算
  • B.线程在等待 IO 时会释放 GIL,其他线程可以执行← 正确答案
  • C.GIL 会加速网络请求本身
  • D.线程可以复制 CPU 核心来提升算力

答案:B。等水开不需要炉灶。线程发起网络或磁盘等待时,会主动把“用火权”(GIL)让出来,其他线程趁机执行,并发度大幅提升。这跟 CPU 并行没有关系。

Q4. 在 CPython 下,对纯 CPU 计算任务开 4 个线程,相比单线程的耗时表现是?

单选题 · mid

  • A.大约快 4 倍
  • B.大约快 2 倍
  • C.基本不缩短,甚至可能略慢← 正确答案
  • D.直接抛出 RuntimeError

答案:C。GIL 使同一时刻只有一个线程执行 Python 字节码,4 个线程轮流抢同一口锅,总计算量不变,还多出线程创建和切换的开销,所以加速比约等于 1,甚至更慢。

Q5. queue.Queue 被广泛用于线程间传递数据,原因是?

单选题 · mid

  • A.内部封装了锁和条件变量,开箱即用← 正确答案
  • B.它完全绕过了 GIL
  • C.它内部会自动创建新线程
  • D.它是多进程专用的通信方式

答案:A。Queue 就是官方帮你装好的“传菜窗口”:自带互斥锁保证 put/get 不冲突,用条件变量实现空队列阻塞、满队列限流,不需要自己管理锁。

Q6. 关于可重入锁 RLock,下列说法正确的是?

单选题 · mid

  • A.它的性能一定优于普通 Lock
  • B.同一个线程可以多次 acquire 而不会把自己锁死← 正确答案
  • C.它允许多个线程同时进入临界区
  • D.它不需要调用 release

答案:B。RLock 记录“重入次数”,同一线程先进门再进门也不死锁,适合递归或嵌套调用场景。多线程互斥的本质没变,次数归零后其他线程才能进入。

Q7. sys.setswitchinterval(5e-3) 这行代码的作用是?

单选题 · hard

  • A.设置 GIL 切换检查间隔约为 5 毫秒← 正确答案
  • B.让所有线程每 5 毫秒暂停一次
  • C.把线程优先级固定为 5
  • D.设置线程超时退出时间为 5 毫秒

答案:A。解释器每执行一定时间片就会检查是否有其他线程在等待 GIL,默认约 5 毫秒切换一次。调小后线程切换更频繁、响应更快,但上下文切换开销也更大。这就是“用火权”每隔一小段时间交一次棒的机制。

Q8. 在 CPython + GIL 环境下,下列哪些操作通常被认为是原子的(不需要额外加锁)?

多选题 · mid

  • A.单条 Python 字节码能完成的操作← 正确答案
  • B.x += 1 这样的自增运算
  • C.list.append(x) 这类单步 C 级操作← 正确答案
  • D.先读共享列表再修改其中元素的多步业务逻辑

答案:AC。GIL 保证“执行一条字节码期间不会被其他线程打断”,所以 A 安全;list.append 在 CPython 中是单步 C 操作,通常安全;而 x += 1 是“读-加-写”三步,可能被切换;多步业务逻辑必须加锁保护。

Q9. 哪些做法能有效降低多线程死锁风险?

多选题 · hard

  • A.所有线程按固定顺序获取多把锁← 正确答案
  • B.使用 acquire(timeout=……) 给加锁加超时← 正确答案
  • C.认为只要用 RLock 就一定不会死锁
  • D.尽量用队列传递数据,减少共享锁竞争← 正确答案

答案:ABD。固定加锁顺序打破“循环等待”,超时打破“不可剥夺”和“无限等待”,队列解耦减少共享资源,都是好手段。而 RLock 只解决“同一线程重入”,两个线程各持一把锁互相等对方的经典死锁依然会发生。

Q10. 关于 GIL 与线程切换,下列说法正确的有?

多选题 · extreme

  • A.线程执行阻塞式 IO 等待时通常会释放 GIL← 正确答案
  • B.解释器执行到阻塞式系统调用时,GIL 也会被让出← 正确答案
  • C.持有 GIL 的线程永远不会被切换
  • D.解释器每执行一定数量的字节码就会尝试切换 GIL← 正确答案

答案:ABD。等水开的人会把炉子让出来(IO 释放 GIL),阻塞式系统调用同理;解释器还按时间片定期检查切换,所以 C 的“永远不会被切换”是错误的。切换机制的存在,就是在“内存安全”和“执行效率”之间找平衡。