詳解Python中Sync與Async執(zhí)行速度快慢對(duì)比
前記
Python新的版本中支持了async/await
語(yǔ)法, 很多文章都在說(shuō)這種語(yǔ)法的實(shí)現(xiàn)代碼會(huì)變得很快, 但是這種快是有場(chǎng)景限制的。這篇文章將嘗試簡(jiǎn)單的解釋為何Async
的代碼在某些場(chǎng)景比Sync
的代碼快。
1.一個(gè)簡(jiǎn)單的例子
首先先從一個(gè)例子了解兩種調(diào)用方法的差別, 為了能清晰的看出他們的運(yùn)行時(shí)長(zhǎng)差別, 都讓他們重復(fù)運(yùn)行10000次, 具體代碼如下:
import asyncio import time n_call = 10000 # sync的調(diào)用時(shí)長(zhǎng) def demo(n: int) -> int: return n ** n s_time = time.time() for i in range(n_call): demo(i) print(time.time() - s_time) # async的調(diào)用時(shí)長(zhǎng) async def sub_demo(n: int) -> int: return n ** n async def async_main() -> None: for i in range(n_call): await sub_demo(i) loop = asyncio.get_event_loop() s_time = time.time() loop.run_until_complete(async_main()) print(time.time() - s_time) # 輸出 # 5.310615682601929 # 5.614157438278198
可以看得出來(lái), sync
的語(yǔ)法大家都是很熟悉, 而async
的語(yǔ)法比較不一樣, 函數(shù)需要使用async def
開(kāi)頭, 同時(shí)調(diào)用async def
函數(shù)需要使用await
語(yǔ)法, 運(yùn)行的時(shí)候需要先獲取線程的事件循環(huán), 然后在通過(guò)事件循環(huán)來(lái)運(yùn)行async_main
函數(shù)來(lái)達(dá)到一樣的效果, 但是從運(yùn)行結(jié)果的輸出可以看得出, sync
的語(yǔ)法在這個(gè)場(chǎng)景中比async
的語(yǔ)法速度快了一些些(由于Python的GIL原因, 這里無(wú)法使用多核的性能, 只能以單核來(lái)跑)。
造成這樣的原因是同樣由同一個(gè)線程執(zhí)行的情況下(cpu單核心),async
的調(diào)用還需要經(jīng)過(guò)一些事件循環(huán)的額外調(diào)用, 這會(huì)產(chǎn)生一些小開(kāi)銷, 從而運(yùn)行時(shí)間會(huì)比sync
的慢, 同時(shí)這是一個(gè)純cpu運(yùn)算的示例, 而async
的的優(yōu)勢(shì)在于網(wǎng)絡(luò)io運(yùn)算, 在這個(gè)場(chǎng)景無(wú)法發(fā)揮優(yōu)勢(shì), 但會(huì)在高并發(fā)場(chǎng)景則會(huì)大放光彩, 造成這樣的原因則是因?yàn)?code>async是以協(xié)程運(yùn)行的, sync
是以線程運(yùn)行的。
NOTE: 目前所說(shuō)的async
語(yǔ)法都是支持網(wǎng)絡(luò)io, 而文件系統(tǒng)的異步io還不是非常的完善, 所以文件系統(tǒng)的異步讀寫(xiě)是通過(guò)封裝交給多線程去處理, 而不是協(xié)程。 具體可見(jiàn): https://github.com/python/asyncio/wiki/ThirdParty#filesystem
2.一個(gè)io的例子
為了了解async
在io場(chǎng)景下的運(yùn)行優(yōu)勢(shì), 先假定有一個(gè)io場(chǎng)景--Web后臺(tái)服務(wù)通常需要處理許多請(qǐng)求, 所有請(qǐng)求都是從不同的客戶端發(fā)出的, 示例如圖:
在這種場(chǎng)景下, 客戶端請(qǐng)求都是在短時(shí)間內(nèi)發(fā)出的。 而服務(wù)端為了能夠在短時(shí)間內(nèi)處理大量的請(qǐng)求, 防止處理延遲, 都會(huì)以某種方式來(lái)支持并發(fā)或者并行。
NOTE: 并發(fā),在操作系統(tǒng)中,是指一個(gè)時(shí)間段中有幾個(gè)程序都處于已啟動(dòng)運(yùn)行到運(yùn)行完畢之間,且這幾個(gè)程序都是在同一個(gè)處理機(jī)上運(yùn)行,但任一個(gè)時(shí)刻點(diǎn)上只有一個(gè)程序在處理機(jī)上運(yùn)行。 并行是計(jì)算機(jī)系統(tǒng)中能同時(shí)執(zhí)行兩個(gè)或多個(gè)處理的一種計(jì)算方法。
對(duì)于sync
語(yǔ)法來(lái)說(shuō), 這個(gè)Web后臺(tái)可以通過(guò)進(jìn)程, 線程或者兩者結(jié)合來(lái)實(shí)現(xiàn), 他們的提供并發(fā)/并行的能力會(huì)局限于woker的數(shù)量, 比如當(dāng)有5個(gè)客戶端同時(shí)請(qǐng)求而服務(wù)端只有4個(gè)worker時(shí), 有一個(gè)請(qǐng)求會(huì)進(jìn)入阻塞等待階段, 直到運(yùn)行的4個(gè)worker有一個(gè)被處理完畢。 為了讓服務(wù)器能提供更好的服務(wù), 我們都會(huì)提供足夠多的worker, 同時(shí)由于進(jìn)程具有良好的隔離性且比較每起一個(gè)進(jìn)程都會(huì)占用一份獨(dú)立的資源, 所以都是以幾個(gè)進(jìn)程+大量線程的形式來(lái)提供服務(wù)。
NOTE: 進(jìn)程是最小的資源分配單位, 過(guò)多的進(jìn)程會(huì)占用很多系統(tǒng)資源, 一般的后臺(tái)服務(wù)啟用的進(jìn)程數(shù)量不會(huì)很多, 同時(shí)線程是最小的調(diào)度單位, 所以以下的調(diào)度我都以線程來(lái)描述。
但是這種方式是很耗系統(tǒng)的資源的(相對(duì)于協(xié)程來(lái)說(shuō)), 因?yàn)榫€程的運(yùn)行都是靠cpu來(lái)執(zhí)行的, 而cpu是有限的, 同一時(shí)刻只能支持固定的幾個(gè)worker運(yùn)行, 其他線程則得等待被調(diào)度, 這樣就意味著每個(gè)線程都只能工作一個(gè)時(shí)間分片, 之后就會(huì)被調(diào)度系統(tǒng)控制進(jìn)入阻塞或者就緒階段, 讓位給其他線程, 直到下次獲取時(shí)間分片時(shí)才可以繼續(xù)運(yùn)行。 為了能模擬出同一時(shí)刻內(nèi), 多個(gè)線程同時(shí)運(yùn)行, 且防止其他線程餓死的情況, 線程每次獲得的運(yùn)行時(shí)間很短, 線程間的調(diào)度切換很頻繁, 當(dāng)啟用更多的進(jìn)程和更多的線程時(shí), 調(diào)度就會(huì)更加的頻繁。
不過(guò)調(diào)度線程的開(kāi)銷還不算大, 比較大的開(kāi)銷是調(diào)度線程而產(chǎn)生的下文切換和競(jìng)爭(zhēng)條件(具體可以參考《計(jì)算機(jī)導(dǎo)論》中進(jìn)程調(diào)度相關(guān)的資料, 我這里只是簡(jiǎn)單說(shuō)明), cpu在執(zhí)行代碼時(shí),它需要把數(shù)據(jù)加載到cpu的緩存中去的再運(yùn)行, 當(dāng)cpu運(yùn)行的線程在這個(gè)時(shí)間分片內(nèi)執(zhí)行完成時(shí), 該線程的最新運(yùn)行數(shù)據(jù)就會(huì)保存起來(lái), 然后cpu會(huì)去加載準(zhǔn)備被調(diào)度的線程的數(shù)據(jù), 并運(yùn)行。 雖然這部分暫存數(shù)據(jù)是保存在比內(nèi)存更快, 比內(nèi)存更靠近c(diǎn)pu的寄存器上, 但是寄存器的訪問(wèn)速度也沒(méi)有cpu緩存的訪問(wèn)速度快, 所以cpu在切換運(yùn)行的線程時(shí), 都會(huì)花上一部分時(shí)間用來(lái)裝載數(shù)據(jù)上還有裝載緩存時(shí)的競(jìng)爭(zhēng)問(wèn)題。
對(duì)比線程的調(diào)度產(chǎn)生的上下文切換與搶占式, async
語(yǔ)法實(shí)現(xiàn)的協(xié)程是非搶占式的, 協(xié)程的調(diào)度是依賴于一個(gè)循環(huán)來(lái)控制, 這個(gè)循環(huán)是一個(gè)非常常高效的任務(wù)管理器和調(diào)度器, 由于調(diào)度的是一段代碼的實(shí)現(xiàn)邏輯, 所以cpu的執(zhí)行代碼并不用切換, 也就沒(méi)有上下文切換的開(kāi)銷, 同時(shí), 也不用考慮裝載緩存的競(jìng)爭(zhēng)問(wèn)題。 還是以上面那個(gè)圖為例子, 在服務(wù)開(kāi)始啟動(dòng)時(shí), 會(huì)先啟動(dòng)一個(gè)事件循環(huán), 當(dāng)收到請(qǐng)求時(shí), 它會(huì)創(chuàng)建一個(gè)任務(wù)來(lái)處理客戶端發(fā)送過(guò)來(lái)的請(qǐng)求, 這個(gè)任務(wù)會(huì)從事件循環(huán)獲取到了執(zhí)行權(quán),獨(dú)占整個(gè)線程資源并一直執(zhí)行, 直到遇到需要等待外部事件, 比如等待數(shù)據(jù)庫(kù)返回?cái)?shù)據(jù)的事件, 這時(shí)任務(wù)會(huì)告訴事件循環(huán)自己在等待這個(gè)事件, 然后交出執(zhí)行權(quán), 事件循環(huán)就會(huì)把執(zhí)行權(quán)傳遞給最需要運(yùn)行的任務(wù)。 當(dāng)剛才交出執(zhí)行權(quán)的任務(wù)在后續(xù)收到數(shù)據(jù)庫(kù)事件響應(yīng)時(shí), 事件循環(huán)會(huì)把它安排到就緒列表的第一個(gè)(不同的事件循環(huán)實(shí)現(xiàn)可能不一樣)并在下一次切換執(zhí)行權(quán)時(shí), 把執(zhí)行權(quán)返回給他, 讓他繼續(xù)執(zhí)行, 直到遇到下一個(gè)等待事件。
這種切換協(xié)程的方式稱為協(xié)作式多任務(wù)處理, 由于只會(huì)在單個(gè)進(jìn)程或者單個(gè)線程中運(yùn)行, 切換協(xié)程時(shí)上下文是不用改變的, cpu不用重新讀寫(xiě)緩存, 所以會(huì)節(jié)省一些開(kāi)銷。 從上面可以看出協(xié)作式切換執(zhí)行權(quán)是基于協(xié)程自己主動(dòng)讓出的, 而線程是搶占式的, 線程在沒(méi)遇到io事件時(shí), 也可能從運(yùn)行狀態(tài)轉(zhuǎn)為就緒狀態(tài), 直到再次被調(diào)用, 這樣會(huì)多出很多調(diào)度帶來(lái)的開(kāi)銷, 而協(xié)程是會(huì)一直運(yùn)行, 直到遇到讓步事件才切換, 所以協(xié)程調(diào)度的次數(shù)會(huì)比線程少很多。 同時(shí)可以看出協(xié)程的何時(shí)調(diào)度是由開(kāi)發(fā)者指定(比如上面所說(shuō)的等等數(shù)據(jù)庫(kù)返回事件), 而且是非搶占式的, 這就意味著某個(gè)協(xié)程在運(yùn)行時(shí), 其他協(xié)程是沒(méi)辦法運(yùn)行的, 只能等到運(yùn)行的協(xié)程交出執(zhí)行權(quán), 所以開(kāi)發(fā)者要確保不能讓任務(wù)在cpu上停留太長(zhǎng)時(shí)間,否則剩余的任務(wù)就會(huì)餓死。
3.總結(jié)
在io場(chǎng)景下, io的開(kāi)銷比cpu執(zhí)行代碼邏輯外的開(kāi)銷大很多, 從這里也可以換個(gè)想法思考, 在遇到io的開(kāi)銷時(shí), 代碼邏輯需要進(jìn)行等待, 而cpu是空閑的, 于是就通過(guò)協(xié)程/線程的方式對(duì)于cpu的多路復(fù)用, 壓榨cpu。 假設(shè)sync
語(yǔ)法和async
語(yǔ)法執(zhí)行的代碼邏輯是一樣的, 那么他們執(zhí)行速度快慢的對(duì)比可以轉(zhuǎn)換為協(xié)程與多進(jìn)程/線程的開(kāi)銷對(duì)比, 也就是協(xié)程事件循環(huán)調(diào)度開(kāi)銷與多進(jìn)程/線程的調(diào)度的開(kāi)銷邏輯對(duì)比, 而事件循環(huán)調(diào)度的開(kāi)銷是基本不變(或者變化不大),多進(jìn)程/線程的開(kāi)銷除了比事件循環(huán)調(diào)度的開(kāi)銷大外,還會(huì)隨著worker的量變多而變多, 當(dāng)并發(fā)量高到一定程度時(shí), 多進(jìn)程/多線程的開(kāi)銷會(huì)大于協(xié)程切換的開(kāi)銷, 這時(shí)async
語(yǔ)法的執(zhí)行速度就會(huì)快于sync
語(yǔ)法。 所以在普通場(chǎng)景下, sync
語(yǔ)法的執(zhí)行速度會(huì)快于async
語(yǔ)法的執(zhí)行速度, 但在io計(jì)算大于cpu計(jì)算且高并發(fā)場(chǎng)景下時(shí), async
語(yǔ)法的執(zhí)行速度會(huì)比sync
語(yǔ)法速度還快。
到此這篇關(guān)于詳解Python中Sync與Async執(zhí)行速度快慢對(duì)比的文章就介紹到這了,更多相關(guān)Python Sync Async內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python實(shí)現(xiàn)讀取及寫(xiě)入csv文件的方法示例
這篇文章主要介紹了Python實(shí)現(xiàn)讀取及寫(xiě)入csv文件的方法,涉及Python針對(duì)csv格式文件的讀取、遍歷、寫(xiě)入等相關(guān)操作技巧,需要的朋友可以參考下2018-01-01使用Python實(shí)現(xiàn)分組數(shù)據(jù)并保存到單獨(dú)的文件中
當(dāng)處理大型數(shù)據(jù)集時(shí),通常需要將數(shù)據(jù)分組,并將每個(gè)分組的數(shù)據(jù)保存到單獨(dú)的文件中,本文將使用 Python 中的 pandas 庫(kù)來(lái)實(shí)現(xiàn)這一目標(biāo),需要的可以參考下2024-04-04在Python web中實(shí)現(xiàn)驗(yàn)證碼圖片代碼分享
這篇文章主要介紹了在Python web中實(shí)現(xiàn)驗(yàn)證碼圖片代碼分享,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11Python如何檢驗(yàn)樣本是否服從正態(tài)分布
這篇文章主要介紹了Python如何檢驗(yàn)樣本是否服從正態(tài)分布問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02Python實(shí)現(xiàn)一個(gè)論文下載器的過(guò)程
這篇文章主要介紹了Python實(shí)現(xiàn)一個(gè)論文下載器的過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01python中py文件與pyc文件相互轉(zhuǎn)換的方法實(shí)例
pyc是一種二進(jìn)制文件,是由py文件經(jīng)過(guò)編譯后,生成的文件,下面這篇文章主要給大家介紹了關(guān)于python中py文件與pyc文件相互轉(zhuǎn)換的相關(guān)資料,需要的朋友可以參考下2022-05-0590行Python代碼開(kāi)發(fā)個(gè)人云盤(pán)應(yīng)用
這篇文章主要介紹了90行Python代碼開(kāi)發(fā)個(gè)人云盤(pán)應(yīng)用,幫助大家更好的理解和學(xué)習(xí)python,感興趣的朋友可以了解下2021-04-04Python利用matplotlib.pyplot繪圖時(shí)如何設(shè)置坐標(biāo)軸刻度
Matplotlib是Python提供的一個(gè)二維繪圖庫(kù),所有類型的平面圖,包括直方圖、散點(diǎn)圖、折線圖、點(diǎn)圖、熱圖以及其他各種類型,都能由Python制作出來(lái)。本文主要介紹了關(guān)于Python利用matplotlib.pyplot繪圖時(shí)如何設(shè)置坐標(biāo)軸刻度的相關(guān)資料,需要的朋友可以參考下。2018-04-04