Python GIL
全局解释器锁GIL,担心它会影响到多线程程序的执行性能。尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。 实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势(比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。
理性看待
GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的。
- 清楚执行的计算的特点
对于依赖CPU的程序,优化底层算法要比使用多线程运行快得多。由于Python是解释执行的,如果你将那些性能瓶颈代码移到一个C语言扩展模块中, 速度也会提升的很快。如果你要操作数组,那么使用NumPy这样的扩展会非常的高效。 最后,你还可以考虑下其他可选实现方案,比如PyPy,它通过一个JIT编译器来优化执行效率 。
- 清楚执行的计算的特点
解决方案
multiprocessing模块
创建一个进程池, 并像协同处理器一样的使用它。当一个线程想要执行CPU密集型工作时,会将任务发给进程池。 然后进程池会在另外一个进程中启动一个单独的Python解释器来工作。 当线程等待结果的时候会释放GIL。 并且,由于计算任务在单独解释器中执行,那么就不会受限于GIL了。 在一个多核系统上面,你会发现这个技术可以让你很好的利用多CPU的优势。
1
# Processing pool (see below for initiazation)
2
pool = None
3
4
# Performs a large calculation (CPU bound)
5
def some_work(args):
6
...
7
return result
8
9
# A thread that calls the above function
10
def some_thread():
11
while True:
12
...
13
r = pool.apply(some_work, (args))
14
...
15
16
# Initiaze the pool
17
if __name__ == '__main__':
18
import multiprocessing
19
pool = multiprocessing.Pool()
C扩展编程技术
主要思想是将计算密集型任务转移给C,跟Python独立,在工作的时候在C代码中释放GIL
1
#include "Python.h"
2
...
3
4
PyObject *pyfunc(PyObject *self, PyObject *args) {
5
...
6
Py_BEGIN_ALLOW_THREADS
7
// Threaded C code
8
...
9
Py_END_ALLOW_THREADS
10
...
11
}
总结
在实际编程中,一出现性能问题不能立马就把锅甩给GIL问题。尤其是多线程网络编程,有可能是其他原因导致的延迟,我们需要实际分析自己的代码是否真的受GIL影响(比如CPU密集计算型),同时我们也要明白GIL大部分都应该只关注CPU的处理而不是I/O。
进程池使用需要解决的问题
为了解决GIL带来的影响,我们利用多进程的时候,不可避免的需要让进程间通信,这涉及到数据序列化和在不同Python解释器通信。被执行的操作需要放在一个通过def语句定义的Python函数中,不能是lambda、闭包可调用实例等, 并且函数参数和返回值必须要兼容pickle。 同样,要执行的任务量必须足够大以弥补额外的通信开销。
混合使用线程和进程池
有时候我们甚至是在混合搭配这两者使用,出了问题很容易让人头疼。如果你要同时使用两者,最好在程序启动时,创建任何线程之前先创建一个单例的进程池。 然后线程使用同样的进程池来进行它们的计算密集型工作。
C扩展
C扩展最重要的特征是它们和Python解释器是保持独立的。 也就是说,如果你准备将Python中的任务分配到C中去执行, 你需要确保C代码的操作跟Python保持独立, 这就意味着不要使用Python数据结构以及不要调用Python的C API。 另外一个就是你要确保C扩展所做的工作是足够的,值得你这样做。 也就是说C扩展担负起了大量的计算任务,而不是少数几个计算。
1 | 这些解决GIL的方案并不能适用于所有问题。 |
2 | 例如,某些类型的应用程序如果被分解为多个进程处理的话并不能很好的工作, 也不能将它的部分代码改成C语言执行。 |
3 | 对于这些应用程序,你就要自己需求解决方案了 (比如多进程访问共享内存区,多解析器运行于同一个进程等)。 |
4 | 或者,你还可以考虑下其他的解释器实现,比如PyPy。 |