欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Python中Sync與Async執(zhí)行速度快慢對(duì)比

 更新時(shí)間:2023年03月01日 14:45:56   作者:so1n  
Python新的版本中支持了async/await語(yǔ)法, 很多文章都在說(shuō)這種語(yǔ)法的實(shí)現(xiàn)代碼會(huì)變得很快, 但是這種快是有場(chǎng)景限制的。這篇文章將嘗試簡(jiǎn)單的解釋為何Async的代碼在某些場(chǎng)景比Sync的代碼快

前記

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)文章

最新評(píng)論