关于Python中GIL的线程就绪队列调度机制及线程结束顺序差异的技术问询
Context
我正在执行一个创建5个线程的Python程序,代码如下:
import time from threading import Thread def do_work(thread_number): print(f"Starting thread {thread_number}") time.sleep(1) print(f"Ending thread {thread_number}") for i in range(5): t = Thread(target=do_work, args=(i,)) t.start()
我多次运行程序后得到了以下不同的输出:
Output 1
Starting thread 0 Starting thread 1 Starting thread 2 Starting thread 3 Starting thread 4 Ending thread 1 Ending thread 0 Ending thread 4 Ending thread 3 Ending thread 2
Output 2
Starting thread 0 Starting thread 1 Starting thread 2 Starting thread 3 Starting thread 4 Ending thread 4 Ending thread 2 Ending thread 3 Ending thread 0 Ending thread 1
Output 3
Starting thread 0 Starting thread 1 Starting thread 2 Starting thread 3 Starting thread 4 Ending thread 4 Ending thread 1 Ending thread 3 Ending thread 2 Ending thread 0
My Question
从输出中可以明显看出,我猜测GIL似乎把被中断的线程放回了等待队列的末尾。这也是为什么每次都是所有Started Thread <thread number>先打印出来,之后才会打印Ending Thread <thread number>。
但既然线程是按顺序启动执行的(先0号,再1号,然后2号……直到4号),而且如果被中断的线程被放到就绪队列的末尾,为什么它们的结束顺序不是和启动顺序相反呢?
究竟是哪些因素导致了这种顺序的变化?
我的解答
首先得纠正你一个关键的误解:CPython的GIL并没有维护一个简单的“等待队列”,也不会把被中断的线程固定放到队列末尾。线程的调度逻辑比你想象的要复杂,是GIL的释放时机和操作系统的线程调度器共同作用的结果:
1. 为什么所有“Starting”都会先全部打印出来?
这和time.sleep(1)的行为直接相关:当Python线程执行到time.sleep()时,会主动释放GIL,然后进入休眠状态。你的主线程启动0号线程后,0号线程打印“Starting”就调用sleep,主动释放GIL;此时主线程可以继续启动1号线程,重复这个过程——直到所有5个线程都完成了“Starting”的打印并进入休眠。这部分顺序固定,完全是因为sleep触发了主动的GIL释放,让主线程能快速启动所有子线程。
2. 为什么结束顺序每次都不一样?
当sleep的1秒结束后,所有休眠的线程会被操作系统唤醒,然后它们需要重新竞争GIL才能继续执行剩下的print("Ending...")。这时候的调度权完全在操作系统的线程调度器手里,和GIL自己的规则无关:
- 操作系统的线程调度器会根据线程的优先级、当前系统的负载、时间片分配策略(比如轮转调度、优先级调度)等多种因素,选择下一个获得CPU时间片并拿到GIL的线程。
- CPython在3.2版本之后,GIL的释放是通过定时器触发的(默认每100个字节码指令释放一次),但线程唤醒后的竞争没有固定顺序,也不是简单的“先中断的放最后”。
- 另外,不同线程从唤醒到实际拿到GIL的时间也可能有细微差异,这也会影响最终的结束顺序。
3. 关于你提到的“中断线程放最后”的误区
你以为GIL会把被中断的线程放到队列末尾,但实际上CPython并没有为等待GIL的线程维护一个有序队列。当一个线程释放GIL后,所有等待的线程会进入竞争状态,操作系统会选择其中一个线程来执行——这个选择不是FIFO,也不是固定把之前的线程排到最后,完全由操作系统的调度策略决定。
总结一下:你看到的结束顺序差异,本质是操作系统线程调度器的“随机”选择导致的,和GIL本身的队列无关。GIL只是保证同一时刻只有一个Python字节码执行流,但线程的调度最终还是由操作系统说了算。
备注:内容来源于stack exchange,提问作者Deepak Tatyaji Ahire

