- 原文地址:Intro to Threads and Processes in Python
- 原文做者:Brendan Fortuner
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:lsvih
- 校對者:yqian1991
在參加 Kaggle 的 Understanding the Amazon from Space 比賽時,我試圖對本身代碼的各個部分進行加速。速度在 Kaggle 比賽中相當重要。高排名經常須要嘗試數百種模型結構與超參組合,能在一個持續一分鐘的 epoch 中省出 10 秒都是一個巨大的勝利。html
讓我吃驚的是,數據處理是最大的瓶頸。我用了 Numpy 的矩陣旋轉、矩陣翻轉、縮放及裁切等操做,在 CPU 上進行運算。Numpy 和 Pytorch 的 DataLoader 在某些狀況中使用了並行處理。我同時會運行 3 到 5 個實驗,每一個實驗都各自進行數據處理。但這種處理方式看起來效率不高,我但願知道我是否能用並行處理來加快全部實驗的運行速度。前端
簡單來講就是在同一時刻作兩件事情,也能夠是在不一樣的 CPU 上分別運行代碼,或者說當程序等待外部資源(文件加載、API 調用等)時把「浪費」的 CPU 週期充分利用起來提升效率。python
下面的例子是一個「正常」的程序。它會使用單線程,依次進行下載一個 URL 列表的內容。linux
下面是一個一樣的程序,不過使用了 2 個線程。它把 URL 列表分給不一樣的線程,處理速度幾乎翻倍。android
若是你對如何繪製以上圖表感到好奇,能夠參考源碼,下面也簡單介紹一下:ios
URLS = [url1, url2, url3, ...]
def download(url, base):
start = time.time() - base
resp = urlopen(url)
stop = time.time() - base
return start,stop
複製代碼
results = [download(url, 1) for url in URLS]
複製代碼
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 的併發庫同樣能夠返回結果數組。git
一個進程就是一個程序的實例(好比 Jupyter notebook 或 Python 解釋器)。進程啓動線程(子進程)來處理一些子任務(好比按鍵、加載 HTML 頁面、保存文件等)。線程存活於進程內部,線程間共享相同的內存空間。github
舉例:Microsoft Word
當你打開 Word 時,你其實就是建立了一個進程。當你開始打字時,進程啓動了一些線程:一個線程專門用於獲取鍵盤輸入,一個線程用於顯示文本,一個線程用於自動保存文件,還有一個線程用於拼寫檢查。在啓動這些線程以後,Word 就能更好的利用空閒的 CPU 時間(等待鍵盤輸入或文件加載的時間)讓你有更高的工做效率。編程
*
)CPU,或者說處理器,管理着計算機最基本的運算工做。CPU 有一個或着多個核,可讓 CPU 同時執行代碼。後端
若是隻有一個核,那麼對 CPU 密集型任務(好比循環、運算等)不會有速度的提高。操做系統須要在很小的時間片在不一樣的任務間來回切換調度。所以,作一些很瑣碎的操做(好比下載一些圖片)時,多任務處理反而會下降處理性能。這個現象的緣由是在啓動與維護多個任務時也有性能的開銷。
CPython(python 的標準實現)有一個叫作 GIL(全局解釋鎖)的東西,會阻止在程序中同時執行兩個線程。一些人很是不喜歡它,但也有一些人喜歡它。目前有一些解決它的方法,不過 Numpy 之類的庫大都是經過執行外部 C 語言代碼來繞過這種限制。
*
對於點積等某些運算,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 調用,多線程明顯比串行處理與多進程速度要快不少。
def download(url):
try:
resp = urlopen(url)
except Exception as e:
print ('ERROR: %s' % e)
複製代碼
2 個線程
4 個線程
2 個進程
4 個進程
我傳入了一個巨大的文本,以觀測線程與進程的寫入性能。線程效果較好,但多進程也讓速度有所提高。
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 個線程
4 個進程
因爲沒有 GIL,能夠在多核上同時執行代碼,多進程理所固然的勝出。
def cpu_heavy(n):
count = 0
for i in range(n):
count += i
複製代碼
串行: 4.2 秒
4 個線程: 6.5 秒
4 個進程: 1.9 秒
不出所料,不管是用多線程仍是多進程都不會對此代碼有什麼幫助。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 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。