摘要:批評的人通常都會說的多線程編程太困難了,眾所周知的全局解釋器鎖,或稱使得多個線程的代碼無法同時運行。多線程起步首先讓我們來創(chuàng)建一個名為的模塊。多進程可能比多線程更易使用,但需要消耗更大的內(nèi)存。
批評 Python 的人通常都會說 Python 的多線程編程太困難了,眾所周知的全局解釋器鎖(Global Interpreter Lock,或稱 GIL)使得多個線程的 Python 代碼無法同時運行。因此,如果你并非 Python 開發(fā)者,而是從其他語言如 C++ 或者 Java 轉(zhuǎn)過來的話,你會覺得 Python 的多線程模塊并沒有以你期望的方式工作。但必須澄清的是,只要以一些特定的方式,我們?nèi)匀荒軌蚓帉懗霾l(fā)或者并行的 Python 代碼,并對性能產(chǎn)生完全不同的影響。如果你還不理解什么是并發(fā)和并行,建議你百度或者 Google 或者 Wiki 一下。
在這篇闡述 Python 并發(fā)與并行編程的入門教程里,我們將寫一小段從 Imgur 下載最受歡迎的圖片的 Python 程序。我們將分別使用順序下載圖片和同時下載多張圖片的版本。在此之前,你需要先注冊一個 Imgur 應用。如果你還沒有 Imgur 賬號,請先注冊一個。
這篇教程的 Python 代碼在 3.4.2 中測試通過。但只需一些小的改動就能在 Python 2中運行。兩個 Python 版本的主要區(qū)別是 urllib2 這個模塊。
注:考慮到國內(nèi)嚴酷的上網(wǎng)環(huán)境,譯者測試原作的代碼時直接卡在了注冊 Imgur 賬號這一步。因此為了方便起見,譯者替換了圖片爬取資源。一開始使用的某生產(chǎn)商提供的圖片 API ,但不知道是網(wǎng)絡原因還是其他原因?qū)е鲁绦蛟谧x取最后一張圖片時無法退出。所以譯者一怒之下采取了原始爬蟲法,參考著 requests 和 beautifulsoup4 的文檔爬取了某頭條 253 張圖片,以為示例。譯文中的代碼替換為譯者使用的代碼,如需原始代碼請參考原文 Python Multithreading Tutorial: Concurrency and Parallelism 。
Python 多線程起步首先讓我們來創(chuàng)建一個名為 download.py 的模塊。這個文件包含所有抓取和下載所需圖片的函數(shù)。我們將全部功能分割成如下三個函數(shù):
get_links
download_link
setup_download_dir
第三個函數(shù),setup_download_dir 將會創(chuàng)建一個存放下載的圖片的目錄,如果這個目錄不存在的話。
我們首先結合 requests 和 beautifulsoup4 解析出網(wǎng)頁中的全部圖片鏈接。下載圖片的任務非常簡單,只要通過圖片的 URL 抓取圖片并寫入文件即可。
代碼看起來像這樣:
download.py import json import os import requests from itertools import chain from pathlib import Path from bs4 import BeautifulSoup # 結合 requests 和 bs4 解析出網(wǎng)頁中的全部圖片鏈接,返回一個包含全部圖片鏈接的列表 def get_links(url): req = requests.get(url) soup = BeautifulSoup(req.text, "html.parser") return [img.attrs.get("data-src") for img in soup.find_all("div", class_="img-wrap") if img.attrs.get("data-src") is not None] # 把圖片下載到本地 def download_link(directory, link): img_name = "{}.jpg".format(os.path.basename(link)) download_path = directory / img_name r = requests.get(link) with download_path.open("wb") as fd: fd.write(r.content) # 設置文件夾,文件夾名為傳入的 directory 參數(shù),若不存在會自動創(chuàng)建 def setup_download_dir(directory): download_dir = Path(directory) if not download_dir.exists(): download_dir.mkdir() return download_dir
接下來我們寫一個使用這些函數(shù)一張張下載圖片的模塊。我們把它命名為single.py。我們的第一個簡單版本的 圖片下載器將包含一個主函數(shù)。它會調(diào)用 setup_download_dir 創(chuàng)建下載目錄。然后,它會使用 get_links 方法抓取一系列圖片的鏈接,由于單個網(wǎng)頁的圖片較少,這里抓取了 5 個網(wǎng)頁的圖片鏈接并把它們組合成一個列表。最后調(diào)用 download_link 方法將全部圖片寫入磁盤。這是 single.py 的代碼:
single.py from time import time from itertools import chain from download import setup_download_dir, get_links, download_link def main(): ts = time() url1 = "http://www.toutiao.com/a6333981316853907714" url2 = "http://www.toutiao.com/a6334459308533350658" url3 = "http://www.toutiao.com/a6313664289211924737" url4 = "http://www.toutiao.com/a6334337170774458625" url5 = "http://www.toutiao.com/a6334486705982996738" download_dir = setup_download_dir("single_imgs") links = list(chain( get_links(url1), get_links(url2), get_links(url3), get_links(url4), get_links(url5), )) for link in links: download_link(download_dir, link) print("一共下載了 {} 張圖片".format(len(links))) print("Took {}s".format(time() - ts)) if __name__ == "__main__": main() """ 一共下載了 253 張圖片 Took 166.0219452381134s """
在我的筆記本上,這段腳本花費了 166 秒下載 253 張圖片。請注意花費的時間因網(wǎng)絡的不同會有所差異。166 秒不算太長。但如果我們要下載更多的圖片呢?2530 張而不是 253 張。平均下載一張圖片花費約 1.5 秒,那么 2530 張圖片將花費約 28 分鐘。25300 張圖片將要 280 分鐘。但好消息是通過使用并發(fā)和并行技術,其將顯著提升下載速度。
接下來的代碼示例只給出為了實現(xiàn)并發(fā)或者并行功能而新增的代碼。為了方便起見,全部的 python 腳本可以在 這個GitHub的倉庫 獲取。(注:這是原作者的 GitHub 倉庫,是下載 Imgur 圖片的代碼,本文的代碼存放在這:concurrency-parallelism-demo)。
使用多線程實現(xiàn)并發(fā)和并行線程是大家熟知的使 Python 獲取并發(fā)和并行能力的方式之一。線程通常是操作系統(tǒng)提供的特性。線程比進程要更輕量,且共享大部分內(nèi)存空間。
在我們的 Python 多線程教程中,我們將寫一個新的模塊來替換 single.py 模塊。這個模塊將創(chuàng)建一個含有 8 個線程的線程池,加上主線程一共 9 個線程。我選擇 8 個工作線程的原因是因為我的電腦是 8 核心的。一核一個線程是一個不錯的選擇。但即使是同一臺機器,對于不同的應用和服務也要綜合考慮各種因素來選擇合適的線程數(shù)。
過程基本上面類似,只是多了一個 DownloadWorker 的類,這個類繼承自 Thread。我們覆寫了 run 方法,它執(zhí)行一個死循環(huán),每一次循環(huán)中它先調(diào)用 self.queue.get()方法,嘗試從一個線程安全的隊列中獲取一個圖片的 URL 。在線程從隊列獲取到 URL 之前,它將處于阻塞狀態(tài)。一旦線程獲取到一個 URL,它就被喚醒,并調(diào)用上一個腳本中的 download_link 方法下載圖片到下載目錄中。下載完成后,線程叫發(fā)送完成信號給隊列。這一步非常重要,因為隊列或跟蹤記錄當前隊列中有多少個線程正在執(zhí)行。如果線程不通知隊列下載任務已經(jīng)完成,那么 queue.join() 將使得主線程一直阻塞。
thread_toutiao.py import os from queue import Queue from threading import Thread from time import time from itertools import chain from download import setup_download_dir, get_links, download_link class DownloadWorker(Thread): def __init__(self, queue): Thread.__init__(self) self.queue = queue def run(self): while True: # Get the work from the queue and expand the tuple item = self.queue.get() if item is None: break directory, link = item download_link(directory, link) self.queue.task_done() def main(): ts = time() url1 = "http://www.toutiao.com/a6333981316853907714" url2 = "http://www.toutiao.com/a6334459308533350658" url3 = "http://www.toutiao.com/a6313664289211924737" url4 = "http://www.toutiao.com/a6334337170774458625" url5 = "http://www.toutiao.com/a6334486705982996738" download_dir = setup_download_dir("thread_imgs") # Create a queue to communicate with the worker threads queue = Queue() links = list(chain( get_links(url1), get_links(url2), get_links(url3), get_links(url4), get_links(url5), )) # Create 8 worker threads for x in range(8): worker = DownloadWorker(queue) # Setting daemon to True will let the main thread exit even though the # workers are blocking worker.daemon = True worker.start() # Put the tasks into the queue as a tuple for link in links: queue.put((download_dir, link)) # Causes the main thread to wait for the queue to finish processing all # the tasks queue.join() print("一共下載了 {} 張圖片".format(len(links))) print("Took {}s".format(time() - ts)) if __name__ == "__main__": main() """ 一共下載了 253 張圖片 Took 57.710124015808105s """
在同一機器上運行這段腳本下載相同張數(shù)的圖片花費 57.7 秒,比前一個例子快了約 3 倍。盡管下載速度更快了,但必須指出的是,因為 GIL 的限制,同一時間仍然只有一個線程在執(zhí)行。因此,代碼只是并發(fā)執(zhí)行而不是并行執(zhí)行。其比單線程下載更快的原因是因為下載圖片是 IO 密集型的操作。當下載圖片時處理器便空閑了下來,處理器花費的時間主要在等待網(wǎng)絡連接上。這就是為什么多線程會大大提高下載速度的原因。當當前線程開始執(zhí)行下載任務時,處理器便可以切換到其他線程繼續(xù)執(zhí)行。使用 Python 或者其他擁有 GIL 的腳本語言會降低機器性能。如果的你的代碼是執(zhí)行 CPU 密集型的任務,例如解壓一個 gzip 文件,使用多線程反而會增長運行時間。對于 CPU 密集型或者需要真正并行執(zhí)行的任務我們可以使用 multiprocessing 模塊。
盡管 Python 的標準實現(xiàn) CPython 有 GIL,但不是所有的 python 實現(xiàn)都有 GIL。例如 IronPython,一個基于 。NET 的 Python 實現(xiàn)就沒有 GIL,同樣的,Jython,基于 Java 的 Python 實現(xiàn)也沒有。你可以在 這里 查看 Python 的實現(xiàn)列表。
使用多進程multiprocessing 模塊比 threading 更容易使用,因為我們不用像在上一個例子中那樣創(chuàng)建一個線程類了。我們只需修改一下 main 函數(shù)。
為了使用多進程,我們創(chuàng)建了一個進程池。使用 multiprocessing 提供的 map 方法,我們將一個 URLs 列表傳入進程池,它會開啟 8 個新的進程,并讓每一個進程并行地去下載圖片。這是真正的并行,但也會付出一點代價。代碼運行使用的存儲空間在每個進程中都會復制一份。在這個簡單的例子中當然無關緊要,但對一些大型程序可能會造成大的負擔。
代碼:
process_toutiao.py from functools import partial from multiprocessing.pool import Pool from itertools import chain from time import time from download import setup_download_dir, get_links, download_link def main(): ts = time() url1 = "http://www.toutiao.com/a6333981316853907714" url2 = "http://www.toutiao.com/a6334459308533350658" url3 = "http://www.toutiao.com/a6313664289211924737" url4 = "http://www.toutiao.com/a6334337170774458625" url5 = "http://www.toutiao.com/a6334486705982996738" download_dir = setup_download_dir("process_imgs") links = list(chain( get_links(url1), get_links(url2), get_links(url3), get_links(url4), get_links(url5), )) download = partial(download_link, download_dir) with Pool(8) as p: p.map(download, links) print("一共下載了 {} 張圖片".format(len(links))) print("Took {}s".format(time() - ts)) if __name__ == "__main__": main()
這里補充一點,多進程下下載同樣了花費約 58 秒,和多線程差不多。但是對于 CPU 密集型任務,多進程將發(fā)揮巨大的速度優(yōu)勢。
將任務分配到多臺機器這一節(jié)作者討論了將任務分配到多臺機器上進行分布式計算,由于沒有環(huán)境測試,而且暫時也沒有這個需求,因此略過。感興趣的朋友請參考本文開頭的的原文鏈接。
結論如果你的代碼是 IO 密集型的,選擇 Python 的多線程和多進程差別可能不會太大。多進程可能比多線程更易使用,但需要消耗更大的內(nèi)存。如果你的代碼是 CPU 密集型的,那么多進程可能是不二選擇,特別是對具有多個處理器的的機器而言。
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/45522.html
摘要:所以與多線程相比,線程的數(shù)量越多,協(xié)程性能的優(yōu)勢越明顯。值得一提的是,在此過程中,只有一個線程在執(zhí)行,因此這與多線程的概念是不一樣的。 真正有知識的人的成長過程,就像麥穗的成長過程:麥穗空的時候,麥子長得很快,麥穗驕傲地高高昂起,但是,麥穗成熟飽滿時,它們開始謙虛,垂下麥芒。 ——蒙田《蒙田隨筆全集》 上篇論述了關于python多線程是否是雞肋的問題,得到了一些網(wǎng)友的認可,當然也有...
摘要:中單線程多線程與多進程的效率對比實驗多線程多進程中多線程和多進程的對比是運行在解釋器中的語言,查找資料知道,中有一個全局鎖,在使用多進程的情況下,不能發(fā)揮多核的優(yōu)勢。 title: Python中單線程、多線程與多進程的效率對比實驗date: 2016-09-30 07:05:47tags: [多線程,多進程,Python]categories: [Python] meta: Pyt...
摘要:一般用進程池維護,的設為數(shù)量。多線程爬蟲多線程版本可以在單進程下進行異步采集,但線程間的切換開銷也會隨著線程數(shù)的增大而增大。異步協(xié)程爬蟲引入了異步協(xié)程語法。 Welcome to the D-age 對于網(wǎng)絡上的公開數(shù)據(jù),理論上只要由服務端發(fā)送到前端都可以由爬蟲獲取到。但是Data-age時代的到來,數(shù)據(jù)是新的黃金,毫不夸張的說,數(shù)據(jù)是未來的一切。基于統(tǒng)計學數(shù)學模型的各種人工智能的出現(xiàn)...
摘要:以下這些項目,你拿來學習學習練練手。當你每個步驟都能做到很優(yōu)秀的時候,你應該考慮如何組合這四個步驟,使你的爬蟲達到效率最高,也就是所謂的爬蟲策略問題,爬蟲策略學習不是一朝一夕的事情,建議多看看一些比較優(yōu)秀的爬蟲的設計方案,比如說。 (一)如何學習Python 學習Python大致可以分為以下幾個階段: 1.剛上手的時候肯定是先過一遍Python最基本的知識,比如說:變量、數(shù)據(jù)結構、語法...
摘要:協(xié)程,又稱微線程,纖程。最大的優(yōu)勢就是協(xié)程極高的執(zhí)行效率。生產(chǎn)者產(chǎn)出第條數(shù)據(jù)返回更新值更新消費者正在調(diào)用第條數(shù)據(jù)查看當前進行的線程函數(shù)中有,返回值為生成器庫實現(xiàn)協(xié)程通過提供了對協(xié)程的基本支持,但是不完全。 協(xié)程,又稱微線程,纖程。英文名Coroutine協(xié)程看上去也是子程序,但執(zhí)行過程中,在子程序內(nèi)部可中斷,然后轉(zhuǎn)而執(zhí)行別的子程序,在適當?shù)臅r候再返回來接著執(zhí)行。 最大的優(yōu)勢就是協(xié)程極高...
閱讀 2816·2023-04-25 15:01
閱讀 3044·2021-11-23 10:07
閱讀 3362·2021-10-12 10:12
閱讀 3452·2021-08-30 09:45
閱讀 2191·2021-08-20 09:36
閱讀 3584·2019-08-30 12:59
閱讀 2429·2019-08-26 13:52
閱讀 932·2019-08-26 13:24