[譯] Python 的多執行緒與多程式

lsvih發表於2018-08-28

初學者的並行程式設計指南

[譯] Python 的多執行緒與多程式

在參加 Kaggle 的 Understanding the Amazon from Space 比賽時,我試圖對自己程式碼的各個部分進行加速。速度在 Kaggle 比賽中至關重要。高排名常常需要嘗試數百種模型結構與超參組合,能在一個持續一分鐘的 epoch 中省出 10 秒都是一個巨大的勝利。

讓我吃驚的是,資料處理是最大的瓶頸。我用了 Numpy 的矩陣旋轉、矩陣翻轉、縮放及裁切等操作,在 CPU 上進行運算。Numpy 和 Pytorch 的 DataLoader 在某些情況中使用了並行處理。我同時會執行 3 到 5 個實驗,每個實驗都各自進行資料處理。但這種處理方式看起來效率不高,我希望知道我是否能用並行處理來加快所有實驗的執行速度。

什麼是並行處理?

簡單來說就是在同一時刻做兩件事情,也可以是在不同的 CPU 上分別執行程式碼,或者說當程式等待外部資源(檔案載入、API 呼叫等)時把“浪費”的 CPU 週期充分利用起來提高效率。

下面的例子是一個“正常”的程式。它會使用單執行緒,依次進行下載一個 URL 列表的內容。

[譯] Python 的多執行緒與多程式

下面是一個同樣的程式,不過使用了 2 個執行緒。它把 URL 列表分給不同的執行緒,處理速度幾乎翻倍。

[譯] Python 的多執行緒與多程式

如果你對如何繪製以上圖表感到好奇,可以參考原始碼,下面也簡單介紹一下:

  1. 在你函式內部加上一個計時器,並返回函式執行的起始與結束時間
URLS = [url1, url2, url3, ...]
def download(url, base):
    start = time.time() - base
    resp = urlopen(url)
    stop = time.time() - base
    return start,stop
複製程式碼
  1. 單執行緒程式的視覺化如下:多次執行你的函式,並將多個開始結束的時間儲存下來
results = [download(url, 1) for url in URLS]
複製程式碼
  1. 將 [start, stop] 的結果陣列進行轉置,繪製柱狀圖
def visualize_runtimes(results):
    start,stop = np.array(results).T
    plt.barh(range(len(start)), stop-start, left=start)
    plt.grid(axis=’x’)
    plt.ylabel("Tasks")
    plt.xlabel("Seconds")
複製程式碼

多執行緒的圖表生成方式與此類似。Python 的併發庫一樣可以返回結果陣列。

程式 vs 執行緒

一個程式就是一個程式的例項(比如 Jupyter notebook 或 Python 直譯器)。程式啟動執行緒(子程式)來處理一些子任務(比如按鍵、載入 HTML 頁面、儲存檔案等)。執行緒存活於程式內部,執行緒間共享相同的記憶體空間。

舉例:Microsoft Word
當你開啟 Word 時,你其實就是建立了一個程式。當你開始打字時,程式啟動了一些執行緒:一個執行緒專門用於獲取鍵盤輸入,一個執行緒用於顯示文字,一個執行緒用於自動儲存檔案,還有一個執行緒用於拼寫檢查。在啟動這些執行緒之後,Word 就能更好的利用空閒的 CPU 時間(等待鍵盤輸入或檔案載入的時間)讓你有更高的工作效率。

程式

  • 由作業系統建立,以執行程式
  • 一個程式可以包括多個執行緒
  • 兩個程式可以在 Python 程式中同時執行程式碼
  • 啟動與終止程式需要花費更多的時間,因此用程式比用執行緒的開銷更大
  • 由於程式不共享記憶體空間,因此程式間交換資訊比執行緒間交換資訊要慢很多。在 Python 中,用序列化資料結構(如陣列)的方法進行資訊交換會花費 IO 處理級別的時間。

執行緒

  • 執行緒是在程式內部的類似迷你程式的東西
  • 不同的執行緒共享同樣的記憶體空間,可以高效地讀寫相同的變數
  • 兩個執行緒不能在同一個 Python 程式中執行程式碼(有解決這個問題的方法*

CPU vs 核

CPU,或者說處理器,管理著計算機最基本的運算工作。CPU 有一個或著多個,可以讓 CPU 同時執行程式碼。

如果只有一個核,那麼對 CPU 密集型任務(比如迴圈、運算等)不會有速度的提升。作業系統需要在很小的時間片在不同的任務間來回切換排程。因此,做一些很瑣碎的操作(比如下載一些圖片)時,多工處理反而會降低處理效能。這個現象的原因是在啟動與維護多個任務時也有效能的開銷。

Python 的 GIL 鎖問題

CPython(python 的標準實現)有一個叫做 GIL(全域性解釋鎖)的東西,會阻止在程式中同時執行兩個執行緒。一些人非常不喜歡它,但也有一些人喜歡它。目前有一些解決它的方法,不過 Numpy 之類的庫大都是通過執行外部 C 語言程式碼來繞過這種限制。

何時使用執行緒,何時使用程式?

  • 得益於多核與不存在 GIL,多程式可以加速 CPU 密集型的 Python 程式。
  • 多執行緒可以很好的處理 IO 任務或涉及外部系統的任務,因為執行緒可以將不同的工作高效地結合起來。而程式需要對結果進行序列化才能匯聚多個結果,這需要消耗額外的時間。
  • 由於 GIL 的存在,多執行緒對 CPU 密集的 Python 程式沒有什麼幫助。

*對於點積等某些運算,Numpy 繞過了 Python 的 GIL 鎖,能夠並行執行程式碼。

並行處理示例

Python 的 concurrent.futures 庫用起來輕鬆愉快。你只需要簡單的將函式、待處理的物件列表和併發的數量傳給它即可。在下面幾節中,我會以幾種實驗來演示何時使用執行緒何時使用程式。

def multithreading(func, args, 
                   workers):
    with ThreadPoolExecutor(workers) as ex:
        res = ex.map(func, args)
    return list(res)

def multiprocessing(func, args, 
                    workers):
    with ProcessPoolExecutor(work) as ex:
        res = ex.map(func, args)
    return list(res)
複製程式碼

API 呼叫

對於 API 呼叫,多執行緒明顯比序列處理與多程式速度要快很多。

def download(url):
    try:
        resp = urlopen(url)
    except Exception as e:
        print ('ERROR: %s' % e)
複製程式碼

[譯] Python 的多執行緒與多程式

2 個執行緒

[譯] Python 的多執行緒與多程式

4 個執行緒

[譯] Python 的多執行緒與多程式

2 個程式

[譯] Python 的多執行緒與多程式

4 個程式

[譯] Python 的多執行緒與多程式

IO 密集型任務

我傳入了一個巨大的文字,以觀測執行緒與程式的寫入效能。執行緒效果較好,但多程式也讓速度有所提升。

def io_heavy(text):
    f = open('output.txt', 'wt', encoding='utf-8')
    f.write(text)
    f.close()
複製程式碼

序列

%timeit -n 1 [io_heavy(TEXT,1) for i in range(N)]
>> 1 loop, best of 3: 1.37 s per loop
複製程式碼

4 個執行緒

[譯] Python 的多執行緒與多程式

4 個程式

[譯] Python 的多執行緒與多程式

CPU 密集型任務

由於沒有 GIL,可以在多核上同時執行程式碼,多程式理所當然的勝出。

def cpu_heavy(n):
    count = 0
    for i in range(n):
        count += i
複製程式碼

[譯] Python 的多執行緒與多程式

序列: 4.2 秒
4 個執行緒: 6.5 秒
4 個程式: 1.9 秒

Numpy 中的點積

不出所料,無論是用多執行緒還是多程式都不會對此程式碼有什麼幫助。Numpy 在幕後執行外部的 C 語言程式碼,繞開了 GIL。

def dot_product(i, base):
    start = time.time() - base
    res = np.dot(a,b)
    stop = time.time() - base
    return start,stop

複製程式碼

序列: 2.8 秒
2 個執行緒: 3.4 秒
2 個程式: 3.3 秒

以上實驗的 Notebook 請參考此處,你可以自己來複現這些實驗。

相關資源

以下是我在探索這個主題時的一些參考文章。特別推薦 Nathan Grigg 的這篇部落格,給了我視覺化方法的靈感。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章