1. 程式人生 > 實用技巧 >FastAPI 非同步程式碼、併發和並行

FastAPI 非同步程式碼、併發和並行

作者:麥克煎蛋 出處:https://www.cnblogs.com/mazhiyong/ 轉載請保留這段宣告,謝謝!

我們這裡探討下關於非同步程式碼、並行和併發的一些概念。

一、初探

1、如果我們使用必須用await呼叫的第三方庫,例如:

results = await some_library()

那麼我們就要用async def來定義路徑操作函式:

@app.get('/')
async def read_results():
    results = await some_library()
    return results

注意:我們在基於async def定義的函式內部才能使用await

2、如果第三方庫不支援使用await,那麼我們就用def定義路徑操作函式即可。

@app.get('/')
def results():
    results = some_library()
    return results

3、如果我們的應用不需要與第三方通訊,那麼就用async def來定義路徑操作函式。

4、如果我們不知道怎麼做,那麼就用def來定義路徑操作函式。

無論上述哪種情況,FastAPI都會執行非同步工作並且速度極快。

但如果我們遵循一些操作規範,將會帶來一些效能上的優化。

現代版本的Python通過使用"協程"來實現對"非同步程式碼"的支援,在語法上表現為async

await的使用。

我們以下重點講述的內容為:

  • 非同步程式碼
  • asyncawait
  • 協程

二、非同步程式碼

非同步程式碼通常表示,開發語言有一種方式用來通知計算機(應用)在程式碼的某個地方,必須等待某些事件在其他地方完成。

這裡的某些事件我們稱之為"slow-file"。在等待"slow-file"完成的這個時間段內,計算機可以執行一些別的任務。

然後計算機(應用)一旦有機會就會返回,比如它需要再次等待,或者它完成了在這個地方的所有其他任務。

接下來會檢查所有等待的任務是否已經完成,或者繼續執行應當要完成的任務。

然後它會從等待任務中取走第一個任務繼續執行。

這裡等待的某些事件通常指的是,相對程式計算或者記憶體操作比較耗時的I/O操作,例如

  • 網路通訊
  • 硬碟檔案讀寫
  • 遠端 API 操作
  • 資料庫操作
  • 其他耗時操作

之所以被稱為"非同步"是因為計算機(應用)沒必要為了"同步"等待"slow-file"完成而什麼事情都不做,那樣的話只能等待取到任務結果後才能繼續工作。

實際上,作為一個非同步系統,一旦某些事件完成,這個事件會等待一會以便計算機(應用)返回獲取結果,然後利用執行結果繼續工作。

對於同步系統來說,通常也稱之為"順序模型",因為在切換執行一個不同任務的時候,計算機(應用)嚴格遵循序列中的步驟,即使有些步驟包含了等待。

2.1 併發漢堡

以上討論的非同步程式碼有時候也稱之為"併發",這與"並行"是不同的。

"併發"和"並行"都意味著"不同的事情或多或少在相同的時間發生",但它們的細節是非常不同的。

你和你的朋友去吃快餐,你排隊的時候收銀員按順序為在你之前的顧客點餐。
輪到你的時候,你為自己和朋友點了兩份新潮的漢堡。
然後你付錢。
然後收銀員告訴廚師以便他準備你的漢堡(雖然他可能正在為其他顧客準備漢堡)。
收銀員給你的訂單號。

在等待取餐的時候,你和朋友挑選一個桌子坐下,然後你們交流了很長時間(製作新潮的漢堡比較耗時)。
在和朋友愉快交流的時候,時不時的,你會看一下櫃檯是否顯示了你的訂單號。

在某個時間終於輪到你了,你去櫃檯取回你的漢堡,然後返回到座位和你的朋友分享。
---------------------------------------------------------------------------
在這個故事裡,你可以把自己想象成計算機(應用)。

當你排隊的時候你是空閒的,沒有做什麼有效的工作。但隊伍是很快的,因為收銀員僅僅是收銀和下單。
輪到你的時候,你做了一些有效的工作,你檢視選單和決定點菜內容,然後支付並檢查支付結果,同時確認返回的訂單內容是正確的。
然後,雖然你還沒得到漢堡,但是你和收銀員之間的工作處於"暫停"狀態,因為你不得不等待漢堡製作完成。

雖然你離開了收銀臺,帶著一個號碼返回到了桌子旁,不過你可以把你的注意力切換到你的朋友身上,繼續你們的交流"工作"。
然後你有了一些"有效的"工作,那就是增進你和朋友之間的感情。

當收銀員說漢堡準備好了並且把你的訂單號放在顯示屏上的時候,你並不會立刻跳起來衝過去,因為你知道沒人偷你的漢堡,這是你的訂單號,別人有別人的訂單號。
因此你會等待你的朋友講完她的故事(結束當前的工作),然後微笑著告訴她你要去取漢堡。

然後你來到櫃檯(繼續你最開始的任務),取到漢堡,感謝收銀員,然後返回到座位。現在終於結束了與櫃檯之間的互動任務。
按照順序,現在開啟了一個新的任務,"吃漢堡",但前一個任務"取漢堡"已經完成了。

2.2 並行漢堡

現在我們來看一下什麼是並行漢堡。

你和你的朋友去獲取並行快餐。

你排隊的時候,同時有幾個(暫且認為有8個)收銀員在為顧客下單,這幾個收銀員同時也是廚師。
每個在你前面的顧客必須等待取到漢堡才能離開櫃檯,因為這8個收銀員在為下一個顧客服務之前,必須立刻去準備好當前顧客的漢堡。

輪到你的時候,你下單兩個新潮的漢堡。
你完成支付。
然後收銀員去廚房製作。
你在櫃檯前等待著,這樣就不會有別人能取走你的漢堡,因為並沒有根據訂單號取貨。
這樣在你和你的朋友忙於不讓別人插隊和取走你的漢堡的時候,你們並沒有多餘的精力進行交流。

這是一種"同步"工作,你和收銀員(廚師)之間處於同步狀態。你必須在那裡等到收銀員(廚師)製作完成漢堡然後交給你,否則別人就可能會取走你的漢堡。
經過在櫃檯前的長時間等待後,收銀員(廚師)終於帶著你的漢堡回來了。
你取到漢堡然後返回餐桌和朋友一起就餐。
你享用漢堡。完成你的漢堡任務。

因為在櫃檯前大量的等待時間,你並沒有與你的朋友有很好的交流。
---------------------------------------------------------------------------
在併發漢堡的場景裡,你是一個帶有兩個處理器(你和你的朋友)的計算機(應用),同時在櫃檯前等待了很長時間。
快餐店有8個處理器(收銀員/廚師)。與之相比並發快餐店只有兩個處理器(一個收銀員,一個廚師)。
但最終,併發漢堡的體驗仍然不是最好的。

下面是一個與漢堡相同的並行故事。

直到最近,大部分的銀行還是有多個收銀員和一個長長的隊伍。
所有的收銀員都是處理完當前顧客的所有事情後,才會開始服務下一位。
你不得不在隊伍里長時間等待,否則你就會失去你的機會。

2.3 漢堡結論:

在"與朋友一起吃快餐漢堡"的場景裡,因為有許多時間在等待,這就為併發系統帶來了更多意義。

這也是大多數web應用的常見情景。 許多許多使用者都在等待通過他們不太好的網路來發送請求,然後又等待請求結果的返回。 這種單次的"等待"雖然是毫秒級的,但把它們加起來,最終就導致了大量的等待。
這也是在web應用中使用非同步程式碼的實際意義

許多流行的Python框架(包含Flask和Django)是在Python的新非同步特性存在之前建立的,因此它們對非同步特性的支援並不像最新的特性那麼給力。

非同步特性造就了NodeJS的流行,同時也是Go語言的立足所在。現在我們通過FastAPI框架也能獲得同樣的效能水平。

2.4 併發比並行更好嗎?

不是這樣的,這並不是這個故事的寓意所在。

併發與並行是不同的。只有在某些特定的場景下(比如包含大量的等待)它是較好一些的。

通常對於web應用來說,併發是比並行更好一些的。但這並不是全部。

想象一下下面這個小故事:

你要打掃一個又大又髒的房間!

對了!這才是全部的故事內容!

在這個房間的所有地方,不存在需要等待的事情,只有大量的工作需要完成。
你可以像漢堡範例那樣按順序執行,先打掃起居室,然後打掃廚房,但因為不存在等待,只是不停的打掃和打掃,因此打掃的順序不會有任何影響。
無論是否按照順序或者不按照順序(併發)打掃,你需要完成的全部工作量是相同的,你所花費的全部工時也是相同的。

但是在這種情況下,如果你能帶來8個人(以前叫收銀員/廚師,現在叫清潔工人),每個人(和你一起)各自負責打掃房間的一塊區域,你們就能並行的完成所有的工作,並且更加快速。
這個時候,每一個清潔者(包括你自己)就是一個處理器,各自完成各自負責的工作。

因為大部分的執行時間被實際工作所佔據,並且在計算機中實際工作是被CPU所完成的,我們通常稱這類問題為"CPU bound",也稱之為計算密集型。
---------------------------------------------------------------------------
計算密集型的操作通常涉及到複雜的數學計算。
例如多媒體的處理、機器視覺、機器學習或者深度學習等

我們可以這樣簡單理解併發與並行:

你吃飯吃到一半,電話來了,你一直到吃完了以後才去接,這就說明你不支援併發也不支援並行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完後繼續吃飯,這說明你支援併發。
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支援並行。
併發的關鍵是你有處理多個任務的能力,不一定要同時。
並行的關鍵是你有同時處理多個任務的能力。 

2.5 併發 + 並行: web和機器學習

藉助FastAPI,我們通常可以在web開發中充分利用併發的優勢。

但對於計算密集型的工作(比如機器學習)我們也可以利用到並行和多處理器的優勢。

尤其考慮到Python是資料計算、機器學習以及深度學習的主要語言,這也使得FastAPI非常適用於資料計算/機器學習的web API和應用開發。

三、async and await

現代版本的Python用一種非常直觀的方式來定義非同步程式碼。看起來就像通常的"順序"程式碼在某一時刻進行"等待"操作。

當有一個操作需要等待執行結果的時候,程式碼範例如下:

burgers = await get_burgers(2)

關鍵之處在於使用了await。這裡告訴Python必須等待get_burgers(2)執行完,才能把結果儲存到burgers中。

藉助上述語法,Python將會在這個期間內去執行別的操作(比如接收新的請求)。

await必須在一個支援非同步特性的函式內進行使用,也就是說必須是用async def宣告的函式:

async def get_burgers(number: int):
# Do some asynchronous stuff to create the burgers
    return burgers

而不是用def宣告的函式:

# This is not asynchronous
def get_sequential_burgers(number: int):
# Do some sequential stuff to create the burgers
    return burgers

藉助async def,Python知道在這樣的函式內,必須要注意await表示式。它可以在結果返回前,"暫停"這個函式的執行,先去執行別的任務。

當你呼叫async def函式的時候,你也必須等待(await)它,否則函式將不會執行。

# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)

因此,當我們使用的第三方庫宣告要使用await呼叫的時候,我們必須要建立一個基於async def宣告的路徑操作函式,例如:

@app.get('/burgers')
async def read_burgers():
    burgers = await get_burgers(2)
return burgers

相關技術細節

我們注意到了await操作必須是在async def函式內使用,async def函式也必須在另一個async def函式內使用。

這就像雞生蛋和蛋生雞的問題,我們怎麼呼叫第一個async函式呢?

在FastAPI框架內部我們不用擔心這個問題,因為第一個函式就是我們的路徑操作函式,而FastAPI框架會正確處理這個問題。

在FastAPI框架外關於 async/await 的詳細使用,我們可以參考文件 check the official Python docs

四、關於協程

對於async def函式返回的操作,我們有一個非常花式的術語稱之為"協程"。

Python知道這個操作可以像函式一樣啟動和完成,並且也可以在內部暫停執行,只要存在await操作。

通過asyncawait完成的非同步程式碼通常稱為"協程",這是相對比於Go語言的主要特性,其稱之為"Goroutines"。

五、其他技術細節

5.1 路徑操作函式

當直接通過def宣告一個路徑操作函式的時候,它會執行在一個外部的執行緒池裡並且處於等待狀態,而不是被直接呼叫(這樣往往會阻塞住整個server)。

如果我們以前使用的是與上述工作方式不同的非同步框架,並且習慣了直接定義def函式來獲取微小的效能提升(也許是100納秒),請注意在FastAPI中

這種效果是完全不同的。在這種情況下,我們最好用async def來宣告函式除非路徑操作函式在執行I/O阻塞操作。

無論在哪種情況下,FastAPI都會比你以前所用的框架表現更好一些(或者至少是持平)。

5.2 依賴項

如果依賴項函式是def而不是async def定義的,那麼它也執行在外部的執行緒池中。

你可能有多個依賴項和子依賴項相互依賴,一些是async def定義的,一些是def定義的。這仍然會正常工作。def定義的會在外部執行緒中被呼叫。

5.3 工具函式

其他直接呼叫的工具類函式,無論是async def定義或者是def定義,FastAPI不會影響你呼叫的方式。

這與FastAPI為你呼叫的函式是截然不同的:包括了路徑操作函式和依賴項函式。

如果你的工具類函式是def定義的,那麼它會被直接呼叫,而不會執行在任何執行緒池中;如果是async def定義的,那麼當你呼叫的時候應當使用await操作。

參考文章:

https://fastapi.tiangolo.com/async/