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