當前位置:
首頁 > 最新 > python八榮八恥,並發並行問題解決

python八榮八恥,並發並行問題解決

Python八榮八恥

本文中的腳本在Python3.4.2中測試通過

開始動手

讓我們從創建一個叫「download.py」的Python模塊開始。這個文件包含了獲取圖片列表以及下載這些圖片所需的所有函數。我們將這些功能分成三個單獨的函數:

get_links

download_link

setup_download_dir

第三個函數,「setup_download_dir」,用於創建下載的目標目錄(如果不存在的話)。Imgur的API要求HTTP請求能支持帶有client ID的「Authorization」頭部。你可以從你註冊的Imgur應用的面板上找到這個client ID,而響應會以JSON進行編碼。我們可以使用Python的標準JSON庫去解碼。下載圖片更簡單,你只需要根據它們的URL獲取圖片,然後寫入到一個文件即可。

接下來,你需要寫一個模塊,利用這些函數去逐個下載圖片。我們給它命名為「single.py」。它包含了我們最原始版本的Imgur圖片下載器的主要函數。這個模塊將會通過環境變數「IMGUR_CLIENT_ID」去獲取Imgur的client ID。它將會調用「setup_download_dir」去創建下載目錄。最後,使用get_links函數去獲取圖片的列表,過濾掉所有的GIF和專輯URL,然後用「download_link」去將圖片下載並保存在磁碟中。下面是「single.py」的代碼:

在我的筆記本上,這個腳本花了19.4秒去下載91張圖片。請注意這些數字在不同的網路上也會有所不同。19.4秒並不是非常的長,但是如果我們要下載更多的圖片怎麼辦呢?或許是900張而不是90張。平均下載一張圖片要0.2秒,900張的話大概需要3分鐘。那麼9000張圖片將會花掉30分鐘。好消息是使用了並發或者並行後,我們可以將這個速度顯著地提高。

接下來的代碼示例將只會顯示導入特有模塊和新模塊的import語句。

使用線程

線程是最出名的實現並發和並行的方式之一。操作系統一般提供了線程的特性。線程比進程要小,而且共享同一塊內存空間。

在這裡,我們將寫一個替代「single.py」的新模塊。它將創建一個有八個線程的池,加上主線程的話總共就是九個線程。之所以是八個線程,是因為我的電腦有8個CPU內核,而一個工作線程對應一個內核看起來還不錯。在實踐中,線程的數量是仔細考究的,需要考慮到其他的因素,比如在同一台機器上跑的的其他應用和服務。

在同一個機器上運行這個腳本,下載時間變成了4.1秒!即比之前的例子快4.7倍。雖然這快了很多,但還是要提一下,由於GIL的緣故,在這個進程中同一時間只有一個線程在運行。因此,這段代碼是並發的但不是並行的。而它仍然變快的原因是這是一個IO密集型的任務。進程下載圖片時根本毫不費力,而主要的時間都花在了等待網路上。這就是為什麼線程可以提供很大的速度提升。每當線程中的一個準備工作時,進程可以不斷轉換線程。使用Python或其他有GIL的解釋型語言中的線程模塊實際上會降低性能。如果你的代碼執行的是CPU密集型的任務,例如解壓gzip文件,使用線程模塊將會導致執行時間變長。對於CPU密集型任務和真正的並行執行,我們可以使用多進程(multiprocessing)模塊。

官方的Python實現——CPython——帶有GIL,但不是所有的Python實現都是這樣的。比如,IronPython,使用.NET框架實現的Python就沒有GIL,基於Java實現的Jython也同樣沒有。

多進程模塊比線程模塊更易使用,因為我們不需要像線程示例那樣新增一個類。我們唯一需要做的改變在主函數中。

為了使用多進程,我們得建立一個多進程池。通過它提供的map方法,我們把URL列表傳給池,然後8個新進程就會生成,它們將並行地去下載圖片。這就是真正的並行,不過這是有代價的。整個腳本的內存將會被拷貝到各個子進程中。在我們的例子中這不算什麼,但是在大型程序中它很容易導致嚴重的問題。


你已經知道了線程和多進程模塊可以給你自己的電腦跑腳本時提供很大的幫助,那麼在你想要在不同的機器上執行任務,或者在你需要擴大規模而超過一台機器的的能力範圍時,你該怎麼辦呢?一個很好的使用案例是網路應用的長時間後台任務。如果你有一些很耗時的任務,你不會希望在同一台機器上佔用一些其他的應用代碼所需要的子進程或線程。這將會使你的應用的性能下降,影響到你的用戶們。如果能在另外一台甚至很多台其他的機器上跑這些任務就好了。

Python庫RQ非常適用於這類任務。它是一個簡單卻很強大的庫。首先將一個函數和它的參數放入隊列中。它將函數調用的表示序列化(pickle),然後將這些表示添加到一個Redis列表中。任務進入隊列只是第一步,什麼都還沒有做。我們至少還需要一個能去監聽任務隊列的worker(工作線程)。

第一步是在你的電腦上安裝和使用Redis伺服器,或是擁有一台能正常的使用的Redis伺服器的使用權。接著,對於現有的代碼只需要一些小小的改動。先創建一個RQ隊列的實例並通過redis-py 庫傳給一台Redis伺服器。然後,我們執行「q.enqueue(download_link, download_dir, link)」,而不只是調用「download_link」 。enqueue方法的第一個參數是一個函數,當任務真正執行時,其他的參數或關鍵字參數將會傳給該函數。

最後一步是啟動一些worker。RQ提供了方便的腳本,可以在默認隊列上運行起worker。只要在終端窗口中執行「rqworker」,就可以開始監聽默認隊列了。請確認你當前的工作目錄與腳本所在的是同一個。如果你想監聽別的隊列,你可以執行「rqworker queue_name」,然後將會開始執行名為queue_name的隊列。RQ的一個很好的點就是,只要你可以連接到Redis,你就可以在任意數量上的機器上跑起任意數量的worker;因此,它可以讓你的應用擴展性得到提升。下面是RQ版本的代碼:

然而RQ並不是Python任務隊列的唯一解決方案。RQ確實易用並且能在簡單的案例中起到很大的作用,但是如果有更高級的需求,我們可以使用其他的解決方案(例如 Celery)。

總結

如果你的代碼是IO密集型的,線程和多進程可以幫到你。多進程比線程更易用,但是消耗更多的內存。如果你的代碼是CPU密集型的,多進程就明顯是更好的選擇——特別是所使用的機器是多核或多CPU的。對於網路應用,在你需要擴展到多台機器上執行任務,RQ是更好的選擇。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 魯濱遜 的精彩文章:

python小白變大牛知識點總結

TAG:魯濱遜 |