丁香五月天婷婷久久婷婷色综合91|国产传媒自偷自拍|久久影院亚洲精品|国产欧美VA天堂国产美女自慰视屏|免费黄色av网站|婷婷丁香五月激情四射|日韩AV一区二区中文字幕在线观看|亚洲欧美日本性爱|日日噜噜噜夜夜噜噜噜|中文Av日韩一区二区

您正在使用IE低版瀏覽器,為了您的雷峰網(wǎng)賬號(hào)安全和更好的產(chǎn)品體驗(yàn),強(qiáng)烈建議使用更快更安全的瀏覽器
此為臨時(shí)鏈接,僅用于文章預(yù)覽,將在時(shí)失效
人工智能開發(fā)者 正文
發(fā)私信給skura
發(fā)送

0

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

本文作者: skura 2019-01-31 18:46
導(dǎo)語(yǔ):附有相關(guān)代碼~

雷鋒網(wǎng) AI 科技評(píng)論按,本文是工程師 Jim Anderson 分享的關(guān)于「通過并發(fā)性加快 python 程序的速度」的文章的第二部分,主要內(nèi)容是 I/O 綁定程序加速相關(guān)。

上一篇中,我們已經(jīng)講過了相關(guān)的概念:什么是并發(fā)?什么是并行? I/O 綁定和 CPU 綁定等。在這里,我們將對(duì)一些 python 并發(fā)方法進(jìn)行比較,包括線程、異步和多進(jìn)程,在程序中何時(shí)使用并發(fā)性以及使用哪個(gè)模塊。

當(dāng)然,本文假設(shè)讀者對(duì) python 有一個(gè)基本的了解,并且使用 python3.6 及以上版來(lái)運(yùn)行示例。你可以從 Real python GitHub repo 下載示例。

如何加速 I/O 綁定程序

讓我們從關(guān)注 I/O 綁定程序和一個(gè)常見問題開始:通過網(wǎng)絡(luò)下載內(nèi)容。在我們的例子中,你將從一些站點(diǎn)下載網(wǎng)頁(yè),但這個(gè)過程可能會(huì)產(chǎn)生任何故障。它只是更容易可視化。

同步版本

我們將從這個(gè)任務(wù)的非并發(fā)版本開始。注意,這個(gè)程序需要請(qǐng)求模塊。在運(yùn)行這個(gè)程序之前,你需要運(yùn)行 pip 安裝請(qǐng)求,這可能需要使用 virtualenv 命令。此版本根本不使用并發(fā):

import requests

import time


def download_site(url, session):
   with session.get(url) as response:
       print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
   with requests.Session() as session:
       for url in sites:
           download_site(url, session)


if __name__ == "__main__":
   sites = [
       "http://www.jython.org",
       "http://olympus.realpython.org/dice",
   ] * 80

  start_time = time.time()
   download_all_sites(sites)
   duration = time.time() - start_time
   print(f"Downloaded {len(sites)} in {duration} seconds")

如你所見,這是一個(gè)相當(dāng)短的程序。download_site()可以從 URL 下載內(nèi)容并打印它的大小。要指出的一個(gè)小問題是,我們正在使用來(lái)自 Session 的會(huì)話對(duì)象。

直接從 requests 中使用 get(),但創(chuàng)建一個(gè) Session 對(duì)象允許 requests 執(zhí)行一些花哨的網(wǎng)絡(luò)技巧從而真正加快速度是可能的。

download_all_sites()創(chuàng)建 Session,然后瀏覽站點(diǎn)列表,依次下載每個(gè)站點(diǎn)。最后,它打印出這個(gè)過程花費(fèi)了多長(zhǎng)時(shí)間,這樣你就可以滿意地看到在下面的示例中并發(fā)性對(duì)我們有多大幫助。

這個(gè)程序的處理圖看起來(lái)很像上一節(jié)中的 I/O 綁定圖。

注意:網(wǎng)絡(luò)流量取決于許多因素,這些因素可能在每秒都在變化。我已經(jīng)看到由于網(wǎng)絡(luò)問題,這些測(cè)試案例從一次運(yùn)行跳轉(zhuǎn)到另一次的時(shí)間加倍了。

為什么同步版本很重要

這個(gè)版本的代碼最棒的特點(diǎn)是,它很簡(jiǎn)單,編寫和調(diào)試相對(duì)容易。代碼的思路更加直接,所以你可以預(yù)測(cè)它將如何運(yùn)作。

同步版本的問題

和我們提供的其他解決方案相比,同步版本最大的問題是,它的速度相對(duì)較慢。以下是我的機(jī)器上的最終輸出示例:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

注意:你得到的結(jié)果可能會(huì)和上面有很大差異。運(yùn)行這個(gè)腳本時(shí),需要的時(shí)間從 14.2 秒到 21.9 秒不等。在本文中,時(shí)間取三次運(yùn)行中最快的一次所花的時(shí)間,在這種情況下,兩種方法之間的差異仍然很明顯。

然而,運(yùn)行速度變慢并不總是一個(gè)大問題。如果你正在運(yùn)行的程序使用同步版本運(yùn)行只需要 2 秒,并且很少運(yùn)行,那么可能不需要添加并發(fā)性。

如果你的程序經(jīng)常運(yùn)行怎么辦?如果運(yùn)行程序需要幾個(gè)小時(shí)怎么辦?讓我們繼續(xù)使用線程重寫這個(gè)程序以實(shí)現(xiàn)并發(fā)性。

線程版本

正如你可能猜測(cè)的那樣,編寫線程程序需要付出更多的努力。然而,對(duì)于簡(jiǎn)單的案例,你可能會(huì)驚訝于它所花費(fèi)的額外努力是如此之少。下面是同一個(gè)程序的線程版本:

import concurrent.futures

import requests

import threading

import time


thread_local = threading.local()


def get_session():
   if not getattr(thread_local, "session", None):
       thread_local.session = requests.Session()
   return thread_local.session


def download_site(url):
   session = get_session()
   with session.get(url) as response:
       print(f"Read {len(response.content)} from {url}")


def download_all_sites(sites):
   with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
       executor.map(download_site, sites)


if __name__ == "__main__":
   sites = [
       "http://www.jython.org",
       "http://olympus.realpython.org/dice",
   ] * 80
   start_time = time.time()
   download_all_sites(sites)
   duration = time.time() - start_time
   print(f"Downloaded {len(sites)} in {duration} seconds")

當(dāng)你添加線程時(shí),整體結(jié)構(gòu)是相同的,因此你只需要做一些更改。download_all_sites()從在每個(gè)站點(diǎn)調(diào)用一次函數(shù)改為更復(fù)雜的結(jié)構(gòu)。

在這個(gè)版本中,你正在創(chuàng)建一個(gè) ThreadPoolExecutor,這看起來(lái)很復(fù)雜。我們可以把它分解為:ThreadPoolExecutor=thread+pool+executor。

這個(gè)對(duì)象將創(chuàng)建一個(gè)線程池,每個(gè)線程都可以并發(fā)運(yùn)行。最后,執(zhí)行器會(huì)控制池中每個(gè)線程的運(yùn)行方式和運(yùn)行時(shí)間。請(qǐng)求將在池中執(zhí)行。

標(biāo)準(zhǔn)庫(kù)將 ThreadPoolExecutor 實(shí)現(xiàn)為上下文管理器,這樣你就可以使用 with 語(yǔ)法來(lái)管理線程池的創(chuàng)建和釋放。

一旦有了 ThreadPoolExecutor,就可以很方便地使用它的.map()方法。此方法在列表中的每個(gè)站點(diǎn)上運(yùn)行傳入函數(shù)。最重要的是,它使用所管理的線程池自動(dòng)并發(fā)地運(yùn)行它們。

那些學(xué)習(xí)其他語(yǔ)言,甚至是 python 2 的用戶可能想知道,在處理線程時(shí),通常用來(lái)管理細(xì)節(jié)的對(duì)象和函數(shù)在哪里,比如 thread.start()、thread.join()和 queue。

這些仍然存在,你可以使用它們來(lái)實(shí)現(xiàn)對(duì)線程運(yùn)行方式的細(xì)粒度控制。但是,從 python3.2 開始,標(biāo)準(zhǔn)庫(kù)添加了一個(gè)執(zhí)行器,如果不需要細(xì)粒度的控制,它可以為你管理許多細(xì)節(jié)。

我們的示例中另一個(gè)有趣的變化是,每個(gè)線程都需要?jiǎng)?chuàng)建自己的 requests.session()對(duì)象。當(dāng)你查看請(qǐng)求文檔時(shí),不一定很容易分辨出來(lái),但是讀到這個(gè)問題時(shí),你似乎很清楚每個(gè)線程需要單獨(dú)的 Session。

這是線程處理的一個(gè)有趣又困難的問題之一。因?yàn)椴僮飨到y(tǒng)可以控制一個(gè)任務(wù)何時(shí)被中斷及另一個(gè)任務(wù)何時(shí)開始,所以在線程之間共享的任何數(shù)據(jù)都需要受到保護(hù),保證線程安全。很遺憾,requests.session()不是線程安全的。

根據(jù)數(shù)據(jù)是什么以及如何使用它,有幾種策略可以使數(shù)據(jù)訪問線程安全。其中之一是使用線程安全的數(shù)據(jù)結(jié)構(gòu),如 python 隊(duì)列模塊中的 queue。

另一種策略是線程本地存儲(chǔ)。Threading.local() 創(chuàng)建一個(gè)看起來(lái)像全局的對(duì)象,但它對(duì)于每個(gè)線程來(lái)說(shuō)是不一樣的。在你的示例中,這是通過 threadLocal 和 get_session()完成的:

threadLocal = threading.local()def get_session():
   if getattr(threadLocal, "session", None) is None:
       threadLocal.session = requests.Session()
   return threadLocal.session

ThreadLocal 是在線程模塊中專門解決這個(gè)問題的??雌饋?lái)有點(diǎn)奇怪,但你只想創(chuàng)建這些對(duì)象中的一個(gè),而不是為每個(gè)線程創(chuàng)建一個(gè)對(duì)象。對(duì)象本身負(fù)責(zé)分離不同線程對(duì)不同數(shù)據(jù)的訪問過程。

當(dāng)調(diào)用 get_session()時(shí),它查找的 session 和它運(yùn)行的特定線程是對(duì)應(yīng)的。因此,每個(gè)線程在第一次調(diào)用 get_session()時(shí)將創(chuàng)建一個(gè)會(huì)話,然后后續(xù)在其整個(gè)生命周期內(nèi)簡(jiǎn)單地調(diào)用該會(huì)話。

最后,一個(gè)關(guān)于選擇線程數(shù)的簡(jiǎn)短說(shuō)明。你可以看到示例代碼使用了 5 個(gè)線程。你可以隨意調(diào)整這個(gè)數(shù)字的大小,看看總的時(shí)間是如何變化的。你可能認(rèn)為每次下載只有一個(gè)線程是最快的,但實(shí)際上不是這樣,至少在我的系統(tǒng)中不是這樣。我發(fā)現(xiàn),線程數(shù)目在 5 到 10 個(gè)之間時(shí),速度是最快的。如果超過這個(gè)值,那么創(chuàng)建和銷毀線程所產(chǎn)生的額外開銷將抵消任何節(jié)省時(shí)間所帶來(lái)的好處。

這里的難點(diǎn)在于,正確的線程數(shù)不是從一個(gè)任務(wù)到另一個(gè)任務(wù)中的常量。需要進(jìn)行一些實(shí)驗(yàn)才能得到結(jié)果。

為什么線程版本很重要

它很快!這里是我測(cè)試中最快的一次。記住,非并發(fā)版本需要 14 秒以上的時(shí)間:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

它的執(zhí)行時(shí)序圖如下所示:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

它使用多個(gè)線程同時(shí)向網(wǎng)站發(fā)出多個(gè)打開的請(qǐng)求,允許你的程序重疊等待時(shí)間并更快地獲得最終結(jié)果!

線程版本的問題

正如你從示例中看到的,要實(shí)現(xiàn)這一點(diǎn)需要更多的代碼,而且你真的需要考慮在線程之間需要共享哪些數(shù)據(jù)。

線程可以以巧妙且難以檢測(cè)的方式進(jìn)行交互。這些交互可能導(dǎo)致隨機(jī)的、間歇性的錯(cuò)誤,且這些錯(cuò)誤很難找到。

異步(asyncio)版本

在你開始檢查異步版本示例代碼之前,讓我們?cè)敿?xì)討論一下異步的工作原理。

異步基礎(chǔ)

這將是 asycio 的簡(jiǎn)化版本。這里有許多細(xì)節(jié)被掩蓋了,但它仍然說(shuō)明了它是如何工作的。

asyncio 的一般概念是,一個(gè)被稱為事件循環(huán)的 python 對(duì)象控制每個(gè)任務(wù)的運(yùn)行方式和時(shí)間。這個(gè)對(duì)象清楚地知道每個(gè)任務(wù)處于什么狀態(tài)。實(shí)際上,任務(wù)可以處于許多狀態(tài),但現(xiàn)在讓我們?cè)O(shè)想一個(gè)簡(jiǎn)化的事件循環(huán),它只有兩個(gè)狀態(tài)。

就緒狀態(tài)指的是任務(wù)有工作要做并且準(zhǔn)備運(yùn)行,而等待狀態(tài)意味著任務(wù)正在等待一些外部事情完成,例如網(wǎng)絡(luò)操作。簡(jiǎn)化的事件循環(huán)維護(hù)兩個(gè)任務(wù)列表,分別對(duì)應(yīng)這兩個(gè)狀態(tài)。它選擇一個(gè)已經(jīng)就緒的任務(wù),然后重新開始運(yùn)行。該任務(wù)處于完全控制狀態(tài),直到它將控件送回事件循環(huán)。

當(dāng)正在運(yùn)行的任務(wù)將控制權(quán)交還給事件循環(huán)時(shí),事件循環(huán)將該任務(wù)放入就緒或等待列表,然后遍歷等待列表中的每個(gè)任務(wù),以查看完成 I/O 操作后該任務(wù)是否已就緒。它知道就緒列表中的任務(wù)仍然是就緒狀態(tài),因?yàn)樗鼈兩形催\(yùn)行。

一旦所有的任務(wù)都被重新排序到正確的列表中,事件循環(huán)就會(huì)選擇下一個(gè)要運(yùn)行的任務(wù)。簡(jiǎn)化的事件循環(huán)選擇等待時(shí)間最長(zhǎng)的任務(wù)并運(yùn)行該任務(wù)。此過程重復(fù),直到事件循環(huán)完成。

asyncio 的一個(gè)重要點(diǎn)是,如果不是有意為之,任務(wù)永遠(yuǎn)不會(huì)放棄控制。任務(wù)在執(zhí)行的過程中從不會(huì)被打斷。這使得我們?cè)诋惒街斜仍诰€程中更容易進(jìn)行資源共享。你不需要擔(dān)心線程安全問題。

async 和 await

現(xiàn)在讓我們來(lái)談?wù)勌砑拥?python 中的兩個(gè)新關(guān)鍵字:async 和 await。根據(jù)上面的討論,你可以將 await 視為允許任務(wù)將控制權(quán)交回事件循環(huán)的一種魔力。當(dāng)你的代碼等待函數(shù)調(diào)用時(shí),await 是一個(gè)信號(hào),表明調(diào)用可能需要花費(fèi)一段時(shí)間,并且任務(wù)應(yīng)該放棄控制。

最簡(jiǎn)單的方法是將 async 看作是 python 的標(biāo)志,告訴它將使用 await 定義函數(shù)。在有些情況下,這不是完全正確的,比如異步生成器,但它適用于許多情況,并在開始時(shí)為你提供一個(gè)簡(jiǎn)單的模型。

你將在下一個(gè)代碼中看到的一個(gè)例外是 async with 語(yǔ)句,它通常從你的等待的對(duì)象創(chuàng)建一個(gè)上下文管理器。雖然語(yǔ)義有點(diǎn)不同,但其思想是相同的:將這個(gè)上下文管理器標(biāo)記為可以替換的東西。

我確信你可以想象到,在管理事件循環(huán)和任務(wù)之間的交互時(shí)有一些復(fù)雜性。對(duì)于以 asyncio 開始的開發(fā)人員來(lái)說(shuō),這些細(xì)節(jié)并不重要,但是你需要記住,任何調(diào)用 await 的函數(shù)都需要標(biāo)記為 async。否則將出現(xiàn)語(yǔ)法錯(cuò)誤。雷鋒網(wǎng)

回到代碼

既然你已經(jīng)基本了解了什么是 asyncio,那么讓我們?yōu)g覽一下示例代碼的 asyncio 版本,并了解它是如何工作的。請(qǐng)注意,此版本添加了 aiohtp。在運(yùn)行它之前,應(yīng)該先運(yùn)行 pip install aiohtp:

import asyncio

import time

import aiohttp


async def download_site(session, url):
   async with session.get(url) as response:
       print("Read {0} from {1}".format(response.content_length, url))


async def download_all_sites(sites):
   async with aiohttp.ClientSession() as session:
       tasks = []
       for url in sites:
           task = asyncio.ensure_future(download_site(session, url))
           tasks.append(task)
       await asyncio.gather(*tasks, return_exceptions=True)


if __name__ == "__main__":
   sites = [
       "http://www.jython.org",
       "http://olympus.realpython.org/dice"
,
   ] * 80
   start_time = time.time()
   asyncio.get_event_loop().run_until_complete(download_all_sites(sites))
   duration = time.time() - start_time
   print(f"Downloaded {len(sites)} sites in {duration} seconds")

這個(gè)版本比前兩個(gè)版本要復(fù)雜一些。它有一個(gè)類似的結(jié)構(gòu),但是啟動(dòng)任務(wù)的工作量比創(chuàng)建線程池執(zhí)行器的工作量要多一些。讓我們從示例的頂部開始。

  • download_site()

頂部的 download_site()與線程版本幾乎相同,但函數(shù)定義行上的 async 關(guān)鍵字和實(shí)際調(diào)用 session.get()時(shí)的 async with 關(guān)鍵字除外。稍后你將看到為什么可以在這里傳遞 session,而不是使用線程本地存儲(chǔ)。

  • download_all_sites()

download_all_sites() 中可以看到線程示例中最大的變化。

你可以在所有任務(wù)之間共享會(huì)話,因此該會(huì)話在此處創(chuàng)建為上下文管理器。任務(wù)可以共享會(huì)話,因?yàn)樗鼈兌荚谕痪€程上運(yùn)行。會(huì)話處于錯(cuò)誤狀態(tài)時(shí),一個(gè)任務(wù)無(wú)法中斷另一個(gè)任務(wù)。

在該上下文管理器中,它使用 asyncio.secure_future()創(chuàng)建一個(gè)任務(wù)列表,該列表還負(fù)責(zé)啟動(dòng)它們。創(chuàng)建所有任務(wù)后,此函數(shù)使用 asyncio.gather()完成會(huì)話內(nèi)容的變動(dòng),直到所有任務(wù)完成。

線程代碼的作用與此類似,但在 ThreadPoolExecutor 中可以方便地處理細(xì)節(jié)。當(dāng)前沒有 asyncioPoolExecutor 類。

然而,這里的細(xì)節(jié)中隱藏著一個(gè)小而重要的變化。還記得之前我們討論過要?jiǎng)?chuàng)建的線程數(shù)嗎?在線程示例中,線程的最佳數(shù)量并不明顯。

asyncio 的一個(gè)很酷的優(yōu)點(diǎn)是它的規(guī)模遠(yuǎn)遠(yuǎn)優(yōu)于線程。與線程相比,每項(xiàng)任務(wù)創(chuàng)建所需的資源和時(shí)間要少得多,因此創(chuàng)建和運(yùn)行更多的資源和時(shí)間能很好地工作。這個(gè)例子只是為每個(gè)要下載的站點(diǎn)創(chuàng)建一個(gè)單獨(dú)的任務(wù),這個(gè)任務(wù)運(yùn)行得很好。雷鋒網(wǎng)

  • __main__

最后,異步的本質(zhì)意味著你必須啟動(dòng)事件循環(huán),并告訴它要運(yùn)行哪些任務(wù)。文件底部的__main__部分包含 get_event_loop() 的代碼,然后運(yùn)行 run_until_complete()。如果沒有別的,他們?cè)诿@些函數(shù)方面做得很好。

如果你已經(jīng)更新到 python 3.7,那么 python 核心開發(fā)人員會(huì)為你簡(jiǎn)化這種語(yǔ)法。不需要分辨那種情況下使用 asyncio.get_event_loop(),那種情況下使用 run_until_complete(),你只需使用 asyncio.run()。

為什么 asyncio 版本很重要

它真的很快!在我的機(jī)器上進(jìn)行的所有測(cè)試中,這是代碼運(yùn)行最快的版本:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

執(zhí)行時(shí)序圖與線程示例中所發(fā)生的情況非常相似。只是 I/O 請(qǐng)求都是由同一線程完成的:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

缺少線程池執(zhí)行器,使得這段代碼比線程示例要復(fù)雜一些。在這種情況下,你需要做一些額外的工作來(lái)獲得更好的性能。

還有一個(gè)常見的論點(diǎn)是,在合適的位置添加 async 和 await 是一個(gè)復(fù)雜的問題。在某種程度上,這是事實(shí)。這個(gè)論點(diǎn)的另一個(gè)方面是,它迫使你思考何時(shí)交換給定的任務(wù),這可以幫助你設(shè)計(jì)出一份更好、更快的代碼。

規(guī)模問題在這里也很突出。為每個(gè)站點(diǎn)運(yùn)行上面的線程示例明顯比用少量線程運(yùn)行它慢。運(yùn)行帶有數(shù)百個(gè)任務(wù)的 asyncio 示例并沒有減慢速度。

asyncio 版本的問題

現(xiàn)在 asyncio 有幾個(gè)問題。為了充分利用 asyncio,你需要特殊的 asyncio 版本的庫(kù)。如果你只是使用下載站點(diǎn)的請(qǐng)求,那么速度會(huì)慢得多,因?yàn)檎?qǐng)求不是用來(lái)通知事件循環(huán)它被阻塞了。隨著時(shí)間的推移,這個(gè)問題越來(lái)越少,因?yàn)樵絹?lái)越多的庫(kù)采用 asyncio。

另一個(gè)更微妙的問題是,如果其中一個(gè)任務(wù)不合作,那么協(xié)作多任務(wù)的所有優(yōu)勢(shì)都會(huì)消失。代碼中的一個(gè)小錯(cuò)誤會(huì)導(dǎo)致一個(gè)任務(wù)運(yùn)行,并長(zhǎng)時(shí)間占用處理器,從而使其他需要運(yùn)行的任務(wù)處于等待狀態(tài)。如果任務(wù)沒有將控制權(quán)交還給事件循環(huán),則無(wú)法中斷事件循環(huán)??紤]到這一點(diǎn),讓我們來(lái)看看一種完全不同的并發(fā)、多處理方法。

多處理版本

與前面的方法不同,多處理版本的代碼充分利用了新計(jì)算機(jī)的多個(gè) CPU。讓我們從代碼開始:

import requests

import multiprocessing

import time


session = None


def set_global_session():
   global session
   if not session:
       session = requests.Session()


def download_site(url):
   with session.get(url) as response:
       name = multiprocessing.current_process().name
       print(f"{name}:Read {len(response.content)} from {url}")


def download_all_sites(sites):
   with multiprocessing.Pool(initializer=set_global_session) as pool:
       pool.map(download_site, sites)


if __name__ == "__main__":
   sites = [
       "http://www.jython.org",
       "http://olympus.realpython.org/dice"
,
   ] * 80
   start_time = time.time()
   download_all_sites(sites)
   duration = time.time() - start_time
   print(f"Downloaded {len(sites)} in {duration} seconds")

這比 asyncio 示例短得多,實(shí)際上,它看起來(lái)與線程示例非常相似,但是在我們深入研究代碼之前,讓我們快速了解一下多處理對(duì)你會(huì)有什么幫助。

簡(jiǎn)述多處理

到目前為止,本文中的所有并發(fā)示例都只在計(jì)算機(jī)的單個(gè) CPU 或核上運(yùn)行。其原因與當(dāng)前的 cpython 的設(shè)計(jì)以及所謂的全局解釋器鎖(globalinterpretorlock,簡(jiǎn)稱 gil)有關(guān)。

標(biāo)準(zhǔn)庫(kù)中的多處理設(shè)計(jì)正是為了改變這種狀態(tài)而設(shè)計(jì)的,它使你能在多個(gè) CPU 上運(yùn)行代碼。在高層,它是通過創(chuàng)建一個(gè)新的 python 解釋器實(shí)例在每個(gè) CPU 上運(yùn)行,然后釋放出程序的一部分來(lái)實(shí)現(xiàn)的。

在當(dāng)前的 python 解釋器中啟動(dòng)一個(gè)新線程的速度不如單獨(dú)啟動(dòng)一個(gè) python 解釋器的速度快。這是一個(gè)重要的操作,存在一些限制和困難,但對(duì)某些問題來(lái)說(shuō),它可以產(chǎn)生巨大的差異。

多處理代碼

代碼與我們的同步版本相比有一些小的變化。第一個(gè)區(qū)別位于 download_all_sites()中。它不是簡(jiǎn)單地重復(fù)調(diào)用 download_site(),而是創(chuàng)建一個(gè) multiprocessing.pool 對(duì)象,并讓它將 download_site 映射到不可訪問的站點(diǎn)。和線程示例相比,這點(diǎn)比較相似。

這里所發(fā)生的是,池(pool)創(chuàng)建了許多單獨(dú)的 python 解釋器進(jìn)程,并讓每個(gè)進(jìn)程在某些項(xiàng)上運(yùn)行指定的函數(shù),在我們的例子中是在站點(diǎn)列表上運(yùn)行指定的函數(shù)。主進(jìn)程和其他進(jìn)程之間的通信由多處理模塊為你處理。

創(chuàng)造池的那條線值得你注意。首先,它不指定要在池中創(chuàng)建多少進(jìn)程,盡管這是一個(gè)可選參數(shù)。默認(rèn)情況下,multiprocessing.pool()將確定計(jì)算機(jī)中的 CPU 數(shù)量并與之匹配。這通常是最好的答案,在我們的例子中也是如此。

對(duì)于這個(gè)問題,增加進(jìn)程的數(shù)量并不能提高速度。相反,它實(shí)際上會(huì)降低速度,因?yàn)閱?dòng)和刪除所有這些進(jìn)程的成本大于并行執(zhí)行 I/O 請(qǐng)求的好處。

接下來(lái),我們得到該調(diào)用的 initializer=set_global_session 部分。請(qǐng)記住,池中的每個(gè)進(jìn)程都有自己的內(nèi)存空間,這意味著它們不能共享會(huì)話對(duì)象之類的東西。你不會(huì)希望每次調(diào)用函數(shù)時(shí)都創(chuàng)建新會(huì)話,而是希望為每個(gè)進(jìn)程創(chuàng)建一個(gè)會(huì)話。

初始化功能參數(shù)就是為這種情況而生成的。無(wú)法將返回值從初始值設(shè)定項(xiàng)傳遞回由進(jìn)程 download_site()調(diào)用的函數(shù),但可以初始化全局會(huì)話變量以保存每個(gè)進(jìn)程的單個(gè)會(huì)話。因?yàn)槊總€(gè)進(jìn)程都有自己的內(nèi)存空間,所以每個(gè)進(jìn)程的全局空間都不同。

這就是所有要說(shuō)的啦,其余的代碼與你以前看到的非常相似。

為什么多處理版本很重要

這個(gè)例子的多處理版本非常好,因?yàn)樗鄬?duì)容易啟動(dòng),并且只需要很少的額外代碼。它還充分利用了計(jì)算機(jī)中的 CPU 資源。此代碼的執(zhí)行時(shí)序圖如下所示:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

多處理版本的問題

這個(gè)版本的示例確實(shí)需要一些額外的設(shè)置,而且全局會(huì)話對(duì)象很奇怪。你必須花費(fèi)一些時(shí)間來(lái)考慮在每個(gè)流程中訪問哪些變量。

最后,它明顯比本例中的異步和線程版本慢:

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

這并不奇怪,因?yàn)?I/O 綁定問題并不是多處理存在的真正原因。在進(jìn)入下一節(jié)并查看 CPU 綁定示例時(shí),你將看到更多內(nèi)容。

本文之前還有相關(guān)概念介紹:如何利用并發(fā)性加速你的python程序(一):相關(guān)概念

以及接下來(lái)的一篇是:如何利用并發(fā)性加速你的python程序(三):CPU 綁定程序加速

via:http://www.ozgbdpf.cn/news/201901/JfoLltRClm3bZzuB.html?type=preview

雷峰網(wǎng)版權(quán)文章,未經(jīng)授權(quán)禁止轉(zhuǎn)載。詳情見轉(zhuǎn)載須知

如何利用并發(fā)性加速你的python程序(二):I/O 綁定程序加速

分享:
相關(guān)文章
當(dāng)月熱門文章
最新文章
請(qǐng)?zhí)顚懮暾?qǐng)人資料
姓名
電話
郵箱
微信號(hào)
作品鏈接
個(gè)人簡(jiǎn)介
為了您的賬戶安全,請(qǐng)驗(yàn)證郵箱
您的郵箱還未驗(yàn)證,完成可獲20積分喲!
請(qǐng)驗(yàn)證您的郵箱
立即驗(yàn)證
完善賬號(hào)信息
您的賬號(hào)已經(jīng)綁定,現(xiàn)在您可以設(shè)置密碼以方便用郵箱登錄
立即設(shè)置 以后再說(shuō)