多執行緒允許一個程序同時執行多個執行緒,這些執行緒共享相同的記憶體和資源(見圖表2和4)。
然而,Python的全域解譯器鎖(GIL)限制了多執行緒在CPU密集型任務中的效能。
Python的全域解譯器鎖(GIL)
GIL是一種鎖,只允許一個執行緒在任何時候控制Python解譯器,這意味著同一時間只有一個執行緒可以執行Python的字節碼。
GIL的引入是為了簡化Python中的記憶體管理,因為許多內部操作,例如物件創建,預設並不是線程安全的。如果沒有GIL,試圖訪問共享資源的多個執行緒將需要複雜的鎖或同步機制來防止競爭條件和數據損壞。
什麼時候GIL會成為瓶頸?
對於單執行緒程序,GIL是無關緊要的,因為該執行緒對Python解譯器擁有獨佔訪問權。對於多執行緒的I/O密集型程序,GIL的問題較少,因為執行緒在等待I/O操作時會釋放GIL。對於多執行緒的CPU密集型操作,GIL則成為一個重要的瓶頸。多個執行緒競爭GIL必須輪流執行Python字節碼。
一個有趣的案例是使用time.sleep,Python有效地將其視為I/O操作。time.sleep函數不是CPU密集型的,因為在睡眠期間不涉及主動計算或執行Python字節碼。相反,跟蹤經過的時間的責任被委託給操作系統。在此期間,執行緒釋放GIL,允許其他執行緒運行並利用解譯器。
多進程允許系統並行運行多個進程,每個進程都有自己的記憶體、GIL和資源。在每個進程內,可能有一個或多個執行緒(見圖表3和4)。
多進程繞過了GIL的限制。這使其適合需要大量計算的CPU密集型任務。
然而,由於單獨的記憶體和進程開銷,多進程的資源需求更高。
與執行緒或進程不同,asyncio使用單個執行緒來處理多個任務。
在使用asyncio庫編寫異步代碼時,您將使用async/await關鍵字來管理任務。
關鍵概念
協程:這些是用async def定義的函數。它們是asyncio的核心,表示可以暫停和稍後恢復的任務。事件循環:它管理任務的執行。任務:協程的包裝器。當您希望協程實際開始運行時,您將其轉換為任務——例如,使用asyncio.create_task()。await:暫停協程的執行,將控制權返回給事件循環。
它是如何運作的
Asyncio運行一個事件循環來安排任務。任務在等待某些東西時,例如網絡響應或文件讀取,自願“暫停”自己。在任務暫停期間,事件循環切換到另一個任務,確保不會浪費時間等待。
這使得asyncio非常適合涉及許多小任務的場景,這些任務花費大量時間等待,例如處理成千上萬的網絡請求或管理數據庫查詢。由於所有操作都在單個執行緒上運行,asyncio避免了執行緒切換的開銷和複雜性。
asyncio和多執行緒之間的主要區別在於它們如何處理等待的任務。
多執行緒依賴操作系統在一個執行緒等待時切換執行緒(強制上下文切換)。當一個執行緒在等待時,操作系統會自動切換到另一個執行緒。asyncio使用單個執行緒,並依賴任務在需要等待時“合作”暫停(協作式多任務)。
寫異步代碼的兩種方法:
方法1:await協程
當您直接await一個協程時,當前協程的執行在await語句處暫停,直到被等待的協程完成。任務在當前協程內按順序執行。
當您需要立即獲得協程的結果以繼續下一步時,使用此方法。
雖然這聽起來像是同步代碼,但實際上並不是。在同步代碼中,整個程序在暫停期間會被阻塞。
使用asyncio時,只有當前協程會暫停,而程序的其餘部分可以繼續運行。這使得asyncio在程序層面上是非阻塞的。
範例:
事件循環在fetch_data完成之前暫停當前協程。
async def fetch_data():print(“正在獲取數據…”)await asyncio.sleep(1) # 模擬網絡調用print(“數據獲取完成”)return “data”
async def main():result = await fetch_data() # 當前協程在這裡暫停print(f”結果: {result}”)
asyncio.run(main())
方法2:asyncio.create_task(協程)
協程被安排在背景中並行運行。與await不同,當前協程立即繼續執行,而不等待已安排的任務完成。
已安排的協程在事件循環找到機會時立即開始運行,而無需等待明確的await。
不會創建新的執行緒;相反,協程在與事件循環相同的執行緒內運行,事件循環管理每個任務的執行時間。
這種方法使程序內部能夠實現並發,允許多個任務有效地重疊執行。稍後您需要等待任務以獲取其結果並確保其完成。
當您希望任務並行運行並且不需要立即獲得結果時,使用這種方法。
範例:
當達到asyncio.create_task()這一行時,協程fetch_data()被安排在事件循環可用時立即開始運行。這可能發生在您明確等待任務之前。相比之下,在第一種await方法中,協程僅在達到await語句時開始執行。
總體而言,這使程序通過重疊多個任務的執行變得更有效率。
async def fetch_data():# 模擬網絡調用await asyncio.sleep(1)return “data”
async def main():# 安排fetch_data任務task = asyncio.create_task(fetch_data()) # 模擬進行其他工作await asyncio.sleep(5) # 現在,等待任務以獲取結果result = await task print(result)
asyncio.run(main())
其他重要點
您可以混合同步和異步代碼。由於同步代碼是阻塞的,可以使用asyncio.to_thread()將其卸載到單獨的執行緒中。這使您的程序有效地實現多執行緒。在下面的例子中,asyncio事件循環在主執行緒上運行,而單獨的背景執行緒用於執行sync_task。import asyncioimport time
def sync_task():time.sleep(2)return “完成”
async def main():result = await asyncio.to_thread(sync_task)print(result)
asyncio.run(main())
您應該將計算密集型的CPU任務卸載到單獨的進程中。
這個流程是一個很好的方法來決定何時使用什麼。
多進程- 最適合計算密集型的CPU任務。- 當您需要繞過GIL時——每個進程都有自己的Python解譯器,允許真正的並行性。多執行緒- 最適合快速的I/O密集型任務,因為上下文切換的頻率降低,Python解譯器在更長時間內保持在單個執行緒上- 不適合CPU密集型任務,因為GIL的存在。Asyncio- 理想的慢I/O密集型任務,例如長時間的網絡請求或數據庫查詢,因為它有效地處理等待,使其可擴展。- 不適合CPU密集型任務,除非將工作卸載到其他進程中。
就這樣,朋友們。這個主題還有很多內容要涵蓋,但我希望我已經向您介紹了各種概念,以及何時使用每種方法。
感謝閱讀!我定期撰寫有關Python、軟體開發和我所構建的項目的文章,所以請關注我,以免錯過。下次見! 🙂
本文由 AI 台灣 運用 AI 技術編撰,內容僅供參考,請自行核實相關資訊。
歡迎加入我們的 AI TAIWAN 台灣人工智慧中心 FB 社團,
隨時掌握最新 AI 動態與實用資訊!