真正的python 多線程!一個修飾符讓你的多線程和C語言一樣快

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對性能的影響大嗎

對於需要做大量計算的任務而言,影響是相當大的。我們先來看一段單線程代碼:

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的多線程庫來實現一個多線程計算:

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*。

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. 安裝這個庫:
pip install --upgrade git+https://github.com/PaddlePaddle/PARL.git
  1. 在本地通過命令啓動一個併發服務(只需要啓動一次)
xparl start --port 6006
  1. 寫代碼的時候通過修飾符修飾你要併發的類@parl.remote。
    這裏需要注意的是隻有經過這個修飾符修飾的類纔可以實現併發。
  2. 在代碼最開始的時候通過parl.connect(‘localhost:6006’)來初始化這個包。

最後貼下這個庫的使用文檔:
https://parl.readthedocs.io/en/latest/parallel_training/setup.html
源碼在這裏:
https://github.com/PaddlePaddle/PARL/tree/develop/parl/remote

後續會繼續研究源碼,看下是怎麼做到一個修飾符就能加速的。大家如果讀過了源碼可以一起討論下:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章