概要
以前的 Python 由于有 GIL 锁的存在,不管机器上的 CPU 有多少核心,它只用一个核。也就说 Python 的多线程发挥不出 CPU 的性能。于 2024-10-01 发布的 Python-3.13.0 版本,可以体验去掉 GIL 特性,我编译测试了下,确实强多了!
编译安装 Python-3.13.0 版本
用下面的代码完成下载、编译、安装;
wget wget https://www.python.org/ftp/python/3.13.0/Python-3.13.0.tgz
tar -xvf Python-3.13.0.tgz
cd Python-3.13.0
./configure --disable-gil --enable-optimizations \
--prefix=/usr/local/python-3.13.0 && \
make && \
make install
搞完上面这一套,你就有了一个没有 GIL 的解释器了;但是要说新解释器强在哪里,这个还要从老的解释器说起。
老版本 Python 多线程的假象
由于有 GIL 锁的存在,老版本的 Python 解释器进程中同一时刻至多只有一个线程在执行;也就是说,就算业务代码里面各个线程之间没有同步关系,实际上也并发不了,原因是解释器层面的这个 GIL 锁给你限死了。假设我们有这么一段线程间完全独立且 CPU 密集型的业务代码
#!/usr/bin/env python3
import sys
from datetime import datetime
import threading
ITER_TIMES = 100000000
def foo(times: int = ITER_TIMES):
start_at = datetime.now()
s = 0
for i in range(times):
s = s + 1
end_at = datetime.now()
print(
" python-{} 线程 {} 执行 foo() 执行耗时 = {} s = {} ".format(
sys.version[:6], threading.current_thread().ident, end_at - start_at, s
)
)
def main():
print("程序开始执行")
# 开 10 个线程
threads = [threading.Thread(target=foo) for _ in range(10)]
start_at = datetime.now()
for thread in threads:
thread.start()
for thread in threads:
thread.join()
end_at = datetime.now()
print(
"python-{} 版下 main 函数执行耗时 = {}s ".format(
sys.version[:6], end_at - start_at
)
)
if __name__ == "__main__":
main()
用系统自带的 Python-3.9.9 版本跑起来是这个一个效果,你说它没有多线程吧;从操作系统层面可以看到它是有多线程的。
ps -ef | grep main.py
root 3295935 3258202 93 21:50 pts/4 00:00:18 python main.py
root 3296071 3250835 0 21:50 pts/8 00:00:00 grep --color=auto main.py
ps -Tp 3295935
PID SPID TTY TIME CMD
3295935 3295935 pts/4 00:00:00 python
3295935 3295936 pts/4 00:00:02 python
3295935 3295937 pts/4 00:00:02 python
3295935 3295944 pts/4 00:00:02 python
3295935 3295945 pts/4 00:00:02 python
3295935 3295946 pts/4 00:00:02 python
3295935 3295947 pts/4 00:00:02 python
3295935 3295948 pts/4 00:00:02 python
3295935 3295949 pts/4 00:00:02 python
3295935 3295950 pts/4 00:00:02 python
3295935 3295951 pts/4 00:00:02 python
如果我们这个时候用 top 来观察,会发现它只用了 1 个核心(由于有上下文切换我们看到的就是 2 个核心,各自用 50%; 如果你的机器是 4 核心,那么你应该能看到每个核心都是 25% 的使用率)。
top
top - 21:51:08 up 76 days, 10:22, 3 users, load average: 2.13, 1.92, 1.75
Tasks: 139 total, 1 running, 138 sleeping, 0 stopped, 0 zombie
%Cpu0 : 50.3 us, 1.7 sy, 0.0 ni, 47.7 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 51.3 us, 1.3 sy, 0.0 ni, 47.0 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
MiB Mem : 7432.3 total, 224.5 free, 6160.0 used, 1047.8 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 947.4 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3295935 root 20 0 748620 11088 4756 S 100.3 0.1 1:02.89 python
Python-3.13.0 版本的涅槃
同一个程序我们用新版本跑起来是这样的
ps -ef | grep main.py
root 3296632 3258202 99 21:52 pts/4 00:00:16 /usr/local/python-3.13.0/bin/python3 main.py
root 3296717 3250835 0 21:52 pts/8 00:00:00 grep --color=auto main.py
ps -Tp 3296632
PID SPID TTY TIME CMD
3296632 3296632 pts/4 00:00:00 python3
3296632 3296633 pts/4 00:00:03 python3
3296632 3296634 pts/4 00:00:03 python3
3296632 3296635 pts/4 00:00:03 python3
3296632 3296636 pts/4 00:00:03 python3
3296632 3296637 pts/4 00:00:03 python3
3296632 3296638 pts/4 00:00:03 python3
3296632 3296639 pts/4 00:00:03 python3
3296632 3296640 pts/4 00:00:03 python3
3296632 3296641 pts/4 00:00:03 python3
3296632 3296642 pts/4 00:00:03 python3
还是老样子看 top
top - 21:53:35 up 76 days, 10:24, 3 users, load average: 3.40, 2.47, 1.98
Tasks: 140 total, 1 running, 139 sleeping, 0 stopped, 0 zombie
%Cpu0 : 99.3 us, 0.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7432.3 total, 370.7 free, 6007.6 used, 1054.0 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 1099.8 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3297109 root 20 0 1795452 42880 5288 S 196.7 0.6 0:20.27 python3
可以看到这次每一个核心都用到了接近 100% ,说明这个 GIL 没有了之后,提升还是非常明显的。
Python-3.9.9 vs Python-3.13.0
Python-3.9.9 是系统自带的版本, Python-3.13.0 是我刚编译好的一个版本,我的开发云主机是双核心的,两个不同解释对同一个程序的耗时表现如下。
Python-3.9.9
python main.py
python-3.9.9 线程 139869657892416 执行 foo() 执行耗时 = 0:01:01.385917 s = 100000000
python-3.9.9 线程 139869275801152 执行 foo() 执行耗时 = 0:01:02.075561 s = 100000000
python-3.9.9 线程 139869300979264 执行 foo() 执行耗时 = 0:01:03.367839 s = 100000000
python-3.9.9 线程 139869674677824 执行 foo() 执行耗时 = 0:01:08.089851 s = 100000000
python-3.9.9 线程 139869666285120 执行 foo() 执行耗时 = 0:01:09.167412 s = 100000000
python-3.9.9 线程 139869292586560 执行 foo() 执行耗时 = 0:01:09.030335 s = 100000000
python-3.9.9 线程 139869284193856 执行 foo() 执行耗时 = 0:01:09.061387 s = 100000000
python-3.9.9 线程 139869691463232 执行 foo() 执行耗时 = 0:01:09.642818 s = 100000000
python-3.9.9 线程 139869649499712 执行 foo() 执行耗时 = 0:01:09.646820 s = 100000000
python-3.9.9 线程 139869683070528 执行 foo() 执行耗时 = 0:01:09.967401 s = 100000000
python-3.9.9 版下 main 函数执行耗时 = 0:01:09.978749s
Python-3.13.0
/usr/local/python-3.13.0/bin/python3 main.py
python-3.13.0 线程 140078739351104 执行 foo() 执行耗时 = 0:00:34.497822 s = 100000000
python-3.13.0 线程 140078730958400 执行 foo() 执行耗时 = 0:00:34.534890 s = 100000000
python-3.13.0 线程 140078839526976 执行 foo() 执行耗时 = 0:00:34.689895 s = 100000000
python-3.13.0 线程 140078831134272 执行 foo() 执行耗时 = 0:00:34.766135 s = 100000000
python-3.13.0 线程 140078143764032 执行 foo() 执行耗时 = 0:00:34.783514 s = 100000000
python-3.13.0 线程 140078705780288 执行 foo() 执行耗时 = 0:00:34.808335 s = 100000000
python-3.13.0 线程 140078747743808 执行 foo() 执行耗时 = 0:00:34.878912 s = 100000000
python-3.13.0 线程 140078722565696 执行 foo() 执行耗时 = 0:00:34.864017 s = 100000000
python-3.13.0 线程 140078697387584 执行 foo() 执行耗时 = 0:00:34.831461 s = 100000000
python-3.13.0 线程 140078714172992 执行 foo() 执行耗时 = 0:00:34.879561 s = 100000000
python-3.13.0 版下 main 函数执行耗时 = 0:00:34.923439s
同样的程序耗时由之前的 69s 下降到 34s ,耗时少一半;跟之前只能用一个核心现在能用两个核心,数值上能对上。
结论
从测试来看没有 GIL 之后性能上确实是有明显提升的,至于特定场景下能提升多说少,这个估计业务场景才是大头。