Python 中的併發程序概念課程重點摘要筆記

Concurrency Concepts in Python (Python 中的併發程序概念)

前言

這是 Fred 老師於 Youtube 頻道發佈的 Concurrency Concepts in Python 影片的重點摘要。

什麼是併發(Concurrent)程序?

  • 併發程序是一種程序或演算法的結構化方式,表示程式碼可以以無序或者部分有序的方式執行,而不影響最終結果。

  • 併發程式碼段不一定按特定順序執行,但最終結果保持不變。

  • 例如: 假設我們要計算一個含 N 個數的列表的平均值 x0, x1, …, xN

    • 非併發(單一執行緒)程式:先將所有數相加,然後再求和,最後統計元素個數->讓總和除以元素個數->得到平均值。

    • 併發(多執行緒)演算法: 將所有元素先分群,在每個群內求和,然後再將所有群的和求和,最後統計元素個數->讓總和除以元素個數->得到平均值。

併發程式碼的執行方式

  • 多 CPU 或多核平行執行:

    • 需要多 CPU 或多核

    • 這部分就先不在這裡討論

  • 依據時間分割交錯(Interleaving)方式來平行處理

    • 不倚賴多 CPU,就算只有單 CPU 或單核心也能使用,但每次執行暫停時,必須保存狀態,每次恢復執行,必須載入已保存的狀態,這稱為交叉執行。

    • 每個分段(Fregment)都可視為一個上下文(Context),但每次上下文切換都會伴隨性能上的損失。

    • 每次上下文何時切換,開發者無從預測,而排程器決定何時切換,開發人員也無法控制。

片段1    片段2    片段3

時間軸

CPU 1   CPU 1   CPU 1
  • 併發程式碼的可能狀態:

    • 運行:正在執行程式碼

    • 就緒:可以運行,如果有 CPU

    • 阻塞:等待某事發生(如IO操作)

執行緒(Thread)

  • 每個處理程序(Task/Process)使用一個或多個執行緒執行

  • 至少有一個主執行緒(Main Thread)

  • 併發程式碼可以使用多執行緒執行

  • 執行緒共享處理程序資源(全域資料,打開的檔案等)

  • 執行緒也可以有"私有"資源(局部變數)

  • 可以在多CPU/多核機器上平行運行 可以在多 CPU/多核機器上併發執行 或 使用時間片交叉執行

  • 作業系統有排程器,決定何時運行執行緒

排程器(Scheduler)

  • 決定何時暫停/重啟執行緒

  • 暫停執行緒時,必須保存狀態

  • 恢復執行緒時,必須恢復保存的狀態

  • 這稱為上下文切換,有性能損耗

  • 調度器搶佔執行緒執行權

  • 多工處理:通用術語,表示多個"事情"併發運行,可以是多個處理程序,也可以是多個執行緒

  • 我使用併發這個詞,而不是平行!

多工處理(Multitasking)

  • 是一種概念,運行併發程式碼,可以用多種方式實現

  • Python全域直譯器鎖(GIL)

    • CPython只允許交叉運行多個執行緒 → 由於GIL的存在,執行緒將交叉運行,不會平行

    • 使CPython直譯器更易實現

    • 實際上加速了單執行緒處理程序

    • 通常會顯著降低現有單執行緒Python應用性能 ← 有額外的上下文切換開銷

    • 但對I/O繫結密集型的工作負載有幫助

CPU繫結的工作負載 - 多處理程序

  • 一種擴展CPU繫結負載到多CPU/多核的方法是啟動多個平行處理程序

  • Python 稱之為多處理程序

  • 處理程序狀態不共享 - 它們是獨立的

  • 處理程序間通訊比執行緒難/耗費高

  • 主應用可以生成多個處理程序

  • 可以向生成的處理程序傳遞資料

  • 可以從處理程序獲取結果

  • 不可擴展!僅限於單台機器的CPU/核心

  • 現代計算中,隨著資料集不斷增大,僅限單機器的擴展通常不足

  • 儘管可以使用多處理程序充分利用單台機器

  • 如果需要那個層次的性能,可能需要編寫更複雜的程式碼以跨多台機器擴展

I/O繫結的工作負載 - 多執行緒

  • CPython本質上是單執行緒的,但大多數時間都在等待I/O操作返回結果

  • 即使單執行緒,併發運行程式碼也有意義 → 當一個執行緒等待I/O時切換到另一個執行緒,可以執行其他程式碼

  • 多執行緒併發I/O繫結的碎片 → 應該提高性能

編寫多執行緒程式碼很困難

  • 容易出錯

  • 搶佔式的 - 我們不知道執行緒何時會被中斷

  • 必須小心處理共享狀態 - 很容易出問題

  • 難以偵錯

  • CPython中的多執行緒對CPU繫結的工作負載沒有幫助,但可以幫助I/O繫結的工作負載

  • 是否有更簡單/安全的選擇來編寫I/O繫結工作負載的併發程式碼? → 非同步程式設計

非同步程式設計

  • 使用 asyncio 模組和特殊關鍵字 async、await、yield

  • 是一種協作式多工處理形式,不是搶佔式多工處理

  • 在程式碼中我們指定任務可以在哪裡暫停,允許另一個任務運行

  • 仍是單執行緒執行

  • 可以顯著改善I/O繫結工作負載的性能 可以顯著改善 I/O 綁定工作負載的效能

  • Python非同步程式設計

    • 提供了比多執行緒更簡單/安全的選擇

    • 非同步和多執行緒本質上是單執行緒的(GIL)

    • 性能改進主要針對I/O繫結工作負載 改善主要 I/O 繫結工作負載

    • 比搶佔式多工處理更安全,但增加了程式碼複雜度 比搶佔式多工處理更安全,但增加了程式碼複雜度

    • 需要編寫併發程式碼(就像threading或multiprocessing),但與執行緒不同,我們必須明確告訴 Python 不同碎片如何中斷彼此並協同工作,這給程式碼新增了一點複雜度 - 阻塞和非阻塞程式碼,但總體上比多執行緒簡單,更易偵錯,更易於除錯,但需要第三方I/O庫支援非同步 但需要第三方 I/O 庫支援非同步 Asyncio 事件循環。

      • 非同步事件循環: 其基本思想是我們"註冊"運行程式碼的併發程式碼片段,並間歇性地指示某一行程式碼將是一個好的中斷函數的時機。Python 建立一個單執行緒的事件循環,對這些非同步啟用的函數呼叫稱為任務。我們最終獲得這些需要執行的任務集合。每次運行一個任務,該任務表示它已準備好切換(或完成) - 通常是因為它正在等待I/O響應。事件循環重新獲取控制權,並開始運行另一個任務。這種方式可以在單執行緒中實現併發程式碼,而不需要使用多執行緒。這種方式可以在單執行緒中實現併發程式碼,而不需要使用多執行緒。

總結

  • 多執行緒

    • 共享資料

    • 對CPU繫結工作負載有用

  • 多處理程序 多處理序

    • 沒有共享資料 沒有數據

    • 處理程序間傳遞資料開銷大

    • 對CPU繫結工作負載有用 對 CPU 綁定工作負載有用

  • 非同步

    • 共享資料

    • 協作式的,我們決定何時讓出控制權 協作式的,我們決定何時讓出控制權

    • 對I/O繫結工作負載性能改進有用 對 I/O 綁定工作負載效能改善有用

    • 需要I/O程式碼和庫支援非同步

綜合以上所述,非同步程式設計為 I/O繫結 (I/O Bound)的工作提供了一種更簡單、更安全的編寫併發程式碼的方式。它避免了多執行緒固有的一些問題,比如共享狀態和搶佔式調度所帶來的難以預測行為。通過讓任務協作式地讓出執行權,可以更容易地編寫出高性能的I/O密集型程序。

1個讚