Python3线程按创建顺序调度的原理?Py2与Py3代码差异解析
问题描述
我在Mac上运行以下代码时发现了一个奇怪的现象:在Python 2.7中输出False,但在Python 3.6中输出True。
from threading import Thread def make_huge_list(amount): my_list = [] def add_num(num): my_list.append(num) threads = [Thread(target=add_num, args=(i,)) for i in range(amount)] for t in threads: t.start() for t in threads: t.join() return my_list if __name__ == '__main__': # 检查输出是否有序 print(make_huge_list(100000) == list(range(100000)))
我知道Python 3为了优化调度公平性改进了GIL,但困惑为什么这段代码在Python 3.6中会输出True,同时想知道Python 3为何以及如何按线程创建顺序调度它们。
解答
咱们先澄清一个关键的点:Python 3并没有保证线程会严格按创建顺序执行,你看到的True只是这个特定轻量任务场景下的大概率结果,不是语言层面的承诺。不过结合GIL的改进,我们可以解释为什么这个场景下会出现这样的现象:
Python 2 vs Python 3的GIL调度差异
- Python 2的GIL采用的是基于字节码计数的释放策略:每执行100个字节码(可通过
sys.setcheckinterval调整),解释器就会释放GIL,让其他线程有机会执行。这种策略很容易在密集计算场景下导致线程调度不公平,也可能在你启动线程的循环过程中就触发GIL切换,导致某个后续线程抢在前面的线程完成任务前执行,打乱append的顺序。 - Python 3.2之后,GIL改成了基于时间片的调度逻辑:每个线程默认可以连续执行5毫秒(可通过
sys.setswitchinterval调整),时间片结束后才会释放GIL。同时,新的调度逻辑在处理线程启动、I/O操作等场景时做了优化,让线程调度更公平。
- Python 2的GIL采用的是基于字节码计数的释放策略:每执行100个字节码(可通过
你的代码场景为什么在Python 3中会有序
你的每个线程任务只有一行my_list.append(num),对应的字节码非常少(大概只有5-6条),完全可以在一个时间片内执行完成。而你是按顺序启动线程:先启动处理0的线程,接着是1,直到99999。在Python 3的调度逻辑下,第一个线程启动后,几乎瞬间就完成了append操作并退出,此时调度器才会去调度下一个已经启动的线程。就这样,每个线程都在自己被调度到的第一个时间片内完成了任务,刚好按创建顺序执行了所有append,最终列表和
range(100000)的顺序完全一致。不要依赖这种行为
如果你给add_num函数加一点耗时操作(比如time.sleep(0.0001)),哪怕只是微小的延迟,Python 3也会立刻出现顺序打乱的情况,输出False。多线程的执行顺序本质上是由操作系统和Python解释器的调度器共同决定的,GIL的改进只是让调度更公平,但绝不会保证执行顺序和线程创建顺序一致。
内容的提问来源于stack exchange,提问作者Yoav Glazner

