> Python 多線程因為GIL的存在,導致其速度比單線程還要慢。但是近期我發現了一個相當好用的庫,這個庫只需要增加一個修飾符就可以使原生的python多線程實現真正意義上的並發。本文將和大家一起回顧下GIL對於多線程的影響,以及了解通過一個修飾符就可以實現和C++一樣的多線程。
## GIL的定義
GIL的全稱是global interpreter lock,官方的定義如下:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
從官方的解釋來看,這個全局鎖是用來防止多線程同時執行底層計算代碼的。之所以這么做,是因為底層庫Cpython,在內存管理這塊是線程不安全的。
## GIL有好處嗎
對GIL的第一印象是這東西限制了多線程並發,對python而言是個弊大於利的存在。但是從stackoverflow上的討論來看,這個存在還是相當有必要的。
- 增加了單線程的運行速度
- 可以更方便地整合一些線程不安全的C語言庫到python里面去
首先單線程的運行速度更快了,因為有這個全局鎖的存在,在執行單線程計算的時候不需要再額外增加鎖,減少了不必要的開支。第二個則是可以更好地整合用C語言所寫的python庫。現在其實挺多用C語言寫好底層計算然后封裝提供python接口的,比如數據處理領域的pandas庫,人工智能領域的計算框架Tensorflow或者pytorch,他們的底層計算都是用C語言寫的。由於這個全局鎖的存在,我們可以更方便(安全)地把這些C語言的計算庫整合成一個python包,對外提供python接口。
## GIL對性能的影響大嗎
對於需要做大量計算的任務而言,影響是相當大的。我們先來看一段單線程代碼:
```python
class A(object):
def run(self):
ans = 0
for i in range(100000000):
ans += i
a = A()
for _ in range(5):
a.run()
```
以上這段代碼是跑5次計算,每次計算是從1累加到1千萬,跑這段代碼需要17.46s。
緊接着,我們用python的多線程庫來實現一個多線程計算:
```python
import threading
class A(object):
def run(self):
ans = 0
for i in range(100000000):
ans += i
threads = []
for _ in range(5):
a = A()
th = threading.Thread(target=a.run)
th.start()
threads.append(th)
for th in threads:
th.join()
```
這里我們啟動了5個線程同時計算,然后我們又測試下時間: **41.35**秒!!!這個時候GIL的問題就體現出來了,我們通過多線程來實現並發,結果比單線程慢了2倍多。
### 一個神奇的修飾符
話不多說,我們先來看下代碼。以下這段代碼和上面的多線程代碼幾乎一樣。但是我們要注意到,在類A的定義上面,我們增加了一個修飾符*@parl.remote_class*。
```python
import threading
import parl
@parl.remote_class
class A(object):
def run(self):
ans = 0
for i in range(100000000):
ans += i
threads = []
parl.connect("localhost:6006")
for _ in range(5):
a = A()
th = threading.Thread(target=a.run)
th.start()
threads.append(th)
for th in threads:
th.join()
```
現在我們來看下計算時間:**3.74秒**!!!相比於單線程的17.46s,這里只用了接近1/5的時間(因為我們開了5個線程)。這里是我覺得比較神奇的地方,並沒有做太多的改動,只是在我的單線程類上面增加了一個修飾符,然后用原生的python多線程繼續跑代碼就變得相當快了。
### 完整的使用說明:
1. 安裝這個庫:
```shell
pip install --upgrade git+https://github.com/PaddlePaddle/PARL.git
```
2. 在本地通過命令啟動一個並發服務(只需要啟動一次)
```shell
xparl start --port 6006
```
3. 寫代碼的時候通過修飾符修飾你要並發的類@parl.remote。
這里需要注意的是只有經過這個修飾符修飾的類才可以實現並發。
4. 在代碼最開始的時候通過parl.connect('localhost:6006')來初始化這個包。
最后貼下這個庫的使用文檔:
https://parl.readthedocs.io/en/latest/parallel_training/setup.html
源碼在這里:
https://github.com/PaddlePaddle/PARL/tree/develop/parl/remote
后續會繼續研究源碼,看下是怎么做到一個修飾符就能加速的。大家如果讀過了源碼可以一起討論下:)