Python 中的多线程“并行“

这几天尝试用 Python3 写一个多线程应用,发现了一个之前一直不知道的语言特性:GIL (Global Interpreter Lock) 全局解释器锁。

先写结论:GIL 的存在让 Python 的多线程应用只能实现并发,而不能实现并行。如果想实现并行,只能通过多进程。

关于并发,并行以及 GIL 的细节可以看这篇博客(英文) [1]。这里简单描述一下并发 (Concurrency) 与并行 (Parallelism) 之间的区别。从下图中可以看到在并发中,处理能力并没有得到提升。而并行是真正运用多个处理器同时处理,提升了总体的处理能力。

并发示意图;服务员可以类比为CPU;顾客可以类比为需要处理的工作

并行示意图

在这里,我所希望的是一个可以并行的程序,而不是一个并发程序。我首先想到的就是多线程编程。通过调用 Python 里的 threading 库,利用多线程达到并行。

因此,我们首先实现一个单线程的程序 (test1) 作为benchmark。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
def demo1(N):
flag = True
for i in range(N):
flag = not flag
print('end')

def test1(N):
print("Single thread")
demo1(N)
demo1(N)

test1(int(1e9))

运行时间为90秒。

然后我们实现一个多线程的程序 (test2) 运行,这里我们用两个线程分别运行demo1 。理论上应该会提高一倍的速度。

1
2
3
4
5
6
7
8
9
from threading import Thread
def test2(N):
print("Two thread with Python threading library")
thread1 = Thread(target=demo1, args=(N,))
thread2 = Thread(target=demo1, args=(N,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()

结果,运行时间为79秒!虽然速度是快了,但是明显不及我们预期的一倍。

而性能不及预期的原因就是在一开始提到的全局解释器锁 (GIL)。由于历史原因,Python 并没有锁操作,所以为了防止多个线程之间的读写冲突,Python 使用 GIL 作为全局锁。换句话说,Python 的解释器每次只会执行一个线程。下图展示了 Python 多线程程序真正的运行方式。

Python 多线程运行

可以看出来,每个线程要运行前先请求 GIL,当线程阻塞时(比如 I/O 阻塞),线程释放 GIL,这时,另一个线程会接着运行。因此,Python 的多线程程序在同一时间只有一个线程在运行。多线程 Python 程序只是并发,而不是并行。

这就解释了为什么test2 的速度并不是 test1 的2倍。


实现一个并行的 Python 程序有以下的几种方法:

  • 使用多进程
  • 使用 Cython 等库调用 C++ 代码
  • 更换解释器,并不是所有 Python 解释器都有 GIL,比如 Jython 就不具有 GIL,但是很多库可能就不能用了

因为进程间并不共享内存,所以无需担心并行中的读写冲突问题。进程之间通过 IPC 进行通信。但 IPC 的 overhead 明显会降低程序的性能。所以多进程适用于通信不频繁的并行程序。

这里,我们可以通过 Multiprocessing 来实现一个多进程 Python 程序 (test3)。

1
2
3
4
5
6
7
8
9
10
from multiprocessing import Process

def test3(N):
print("Two process")
p1 = Process(target=demo1, args=(N,))
p2 = Process(target=demo1, args=(N,))
p1.start()
p2.start()
p1.join()
p2.join()

运行时间为42秒!大概是单线程的一半。可见多进程利用多个 CPU 实现了并行计算。

关于 cython 的使用可以参考这篇博客 [2]。但是值得注意的是,利用 cython 只能实现 C++ 部分代码的并行。一旦运行 Python 部分的代码,GIL又回让程序从并行变为并发。

在这里,我并没有尝试使用其他解释器,有兴趣的朋友可以自己试验一下。

关于 GIL 一直有很多讨论,大家可以通过 [3] [4] [5] 更多的了解 GIL 的优缺点。


[1] Concurrent Programming in Python is not what you think it is

[2] 什么,听说3分钟入门Cython??

[3] 12.9 Python的全局锁问题

[4] Python的GIL是什么鬼,多线程性能究竟如何

[5] Python 的多线程原来不是真的多线程啊

Recommended Posts