Python 程序是如何运行的?
- 将源码
*.py
编译成字节码*.pyc
, - 再由 Python 虚拟机逐行解释
*.pyc
成机器码,通知 CPU 执行
名词解释:
- 源码 (Programing Code) - 我们用高级语言敲的代码
- 字节码 (Byte Code) - 将高级语言编译成更低一级的中间代码,方便虚拟机(VM)执行
- 机器码 (Machine Code) - 用于操控 CPU 运行的二进制指令代码
什么是 GIL?
Python 虚拟机默认使用的是 CPython 解释器(C 语言实现),CPython 使用了 GIL (Golbal Iterpreter Lock - 全局解释器锁),来确保同一时间只有一个线程运行,所以即使再多的线程也只能有效的使用一个 CPU。
为什么不删除 GIL?
实验证明,如果放弃 GIL,使用大量细粒度的锁代替,导致单线程性能下降至少 30%。所以说 GIL 在支持多线程的同时能把单线程的优势最大地发挥出来。
GIL 的限制
由于 GIL 的存在,即便是多线程的 Python 程序也无法利用多核 CPU 的优势。不过 I/O 密集型程序,依然适合使用多线程,因为它们大部分时间都在等待 I/O,对 CPU 依赖很低。
名词解释:
- CPU 密集型 - 数学计算、搜索、图像处理等
- I/O 密集型 - 文件操作、网络交互、数据库操作等
怎么充分的利用多核?
使用多进程替代多线程,或者使用 C 扩展
有了 GIL 还需要线程锁吗?
需要。GIL 只是解释器级别的锁,它能保证当多个线程在修改一个变量时不会崩溃,但结果可能是错乱的。
如多个线程修改全部变量i = i + 1
,是先读取 i,再加 1 后,最后写回内存,这会把在读与写期间其他线程对 i 的所有修改覆都盖掉。所以我们需要用线程锁来对整个读写过程加锁。
PyPy 是什么?
Python 语言实现的另一种解释器,使用了 JIT 编译方式,没有 GIL。运行 Pure Python (纯 Python 代码) 程序速度更快。如果依赖了 C 扩展,反而会慢,如 MysqlDB、protobuf 库。
JIT 与其他编译方式的对比:
- Compilation: 先编译成适用于本机识别的机器码,再运行,不同系统或 CPU 需要重新编译 (如 C/C++)
- Interpretation: 解释器在程序运行时把字节码逐行解释成机器码,边解释,边执行 (如 CPython/Ruby)
- JIT Compilation - 及时编译 (Just-in-Time),是对前两张方法的整合,将多次调用的字节码,解释成机器码,缓存起来,优先读缓存,而非解释。支持跨平台的同时,又降低了动态编译对性能的影响。 (如 Java/C#/Pypy)
总结
根据应用场景选择最佳的性能优化方案
- I/O 密集型: 使用多线程
- I/O 密集型 & Pure Python: 使用 Pypy 解释器
- CPU 密集型: 使用多进程或把复杂逻辑用 C 扩展实现