Python-3.12 告别 GIL 锁 & 性能原地飞升!
随着 Python 之父的回归,Python 也是越来越看重性能;GIL 这个老大难问题也提上了日程。从最近的讨论我们可以看到 GIL 在 Python-3.12 之后将会是一个可选项。详细的可以看官方的 PEP 703 提案。
GIL 面对 CPU 密集型场景是真的坑
记得刚开始搞 Python 量化投资的时候,逻辑还比较简单整个模型都是自己手撸,也没有用什么第三方库;那时刚入门的我就发现一个问题;我的程序好像只能用到一个核的算力。
后来才知道是 GIL 坑的我,大意了!凡是过往、皆为序章,就此打住。先来构造一个简单的 CPU 密集型场景,体验一下 GIL 有多坑。
#!/usr/bin/env python3
# -*- encoding: utf8 -*-
"""
测试多线程下 CPU 密集型场景 GIL 的性能表现
"""
from concurrent.futures import ThreadPoolExecutor
def fun_sum(max_number:int = 0):
"""从 0 累加到 max_number -1
Parameter:
----------
max_number: int
Return:
-------
int
"""
if max_number <= 0:
return 0
total = 0
for i in range(max_number):
total = total + i
print("total = {0}".format(total))
return total
def main():
threads = 8
max_number = 10000000000
with ThreadPoolExecutor(max_workers=threads) as executor:
for _ in range(threads):
executor.submit(fun_sum, max_number)
if __name__ == "__main__":
main()
1. 在双核的机器上它的表现如下,也就是说它只能用到一个核心的 100% 。
2. 在 8 核心的机器上它是这样的,也就是说它也只能用到一个核心的 100% 。
这么个老实的程序(不算再多的核心它都只用一个),定然不是我们在 cpu 密集型场景下想要看到的。
之前的解决方案
经过多年的磨合,社区为了临时解决这个 GIL 锁的问题,宏观上大致上有 2 类不同的方案。
1. 使用 C/C+ 编写处理逻辑,在这个里面就完全没有 GIL 的限制了,想怎么玩就怎么玩,非常的自由; 最后只能由 Python 去调用相应的处理逻辑就行。这个对动手能力的要求就比较高。
2. 第二个方案就比较简单,就是直接多开几个进程,不同的进程处理不同的数据。可以说是简单粗暴,直接有效。我们这里用第二个方案演示一下。
# 开两个进程
python3 mult-threads.py &
python3 mult-threads.py &
不优雅就是原罪
前面我们提到的两个绕过 GIL 的方案都不太优雅,优雅的解决方案就应该是把 GIL 锁拿掉。以前也不是没有大牛做过这个事,只是他们都失败了。
这次 703 没有之前那么激进,而是把 GIL 做成一个可选项,在编译时安装时指定要不要编译一个没有 GIL 的版本。另外这次的不同之处在于这个优化进了 PEP ,也就是说这次有官方背书。
希望他们能成功!我用了一个内部的版本测试了下,性能可以说是原地飞升!!!Python 再也不是那个多线程不行小老弟了。
Python 新版本测试
这个新版本的不方便之处就是它要重新编译安装解释器,并且有可能还有一些特殊场景下的兼容性问题要适配,不过我们上面的例子不存在不兼容性的事,可以直接测试。
1. 编译时的关键参数
./configure --prefix=/usr/local/python-nogil --enable-optimizations
2. 代码一行不改还是直接上用线程池
#!/usr/bin/env python3
# -*- encoding: utf8 -*-
"""
测试多线程下 CPU 密集型场景 GIL 的性能表现
"""
from concurrent.futures import ThreadPoolExecutor
def fun_sum(max_number:int = 0):
"""从 0 累加到 max_number -1
Parameter:
----------
max_number: int
Return:
-------
int
"""
if max_number <= 0:
return 0
total = 0
for i in range(max_number):
total = total + i
print("total = {0}".format(total))
return total
def main():
threads = 8
max_number = 10000000000
with ThreadPoolExecutor(max_workers=threads) as executor:
for _ in range(threads):
executor.submit(fun_sum, max_number)
if __name__ == "__main__":
main()
3. 启动程序
python3 mult-threads.py &
4. 观察没有 GIL 的 CPU 使用情况
可以看到这下一个进程占满了所有的 CPU 核心,牛逼++ !