Python 并行計算的那點事(第1部分)(The Python Concurrency Story - Part 1)
英文原文:https://powerfulpython.com/blog/python-concurrency-story-pt1/
本文:https://www.cnblogs.com/popapa/p/python_concurrency1.html
采集日期:2021-05-02
以撰寫軟體為業有一件事很不錯,就是能讓人保持謙卑,我一度以為自己很聰明,并對此有點洋洋自得,直到開始每天寫代碼的日子,才發現并非如此,海森堡 Bug(Heisenbug)就像一個小瘟神一般,靜靜地等著我狂妄自大的那一刻,,,突然一個 Bug 就被我放了進來,為了找到它花了3個小時,修復卻只消1行代碼,
當然,對于很多人來說,能讓我們保持謙卑的緣由就是并行計算(Concurrency),從現在開始,無論對并行計算喜歡與否,作為專業的軟體工程師,我們都不得不對它做出妥善的考慮和理解,這就需要研發出一些思維模型,能夠對其進行清晰地演繹,并且掌握一些為了完成作業所必需的軟體工具,理想情況下,還能漸漸學會少發生一些那種找找3小時、修修1行代碼的 Bug,
雖然基本原理是通用的,但軟體如何實作在很大程度上取決于所用的開發語言,為了實作并行計算,每種開發語言都有著各自的抽象、語法和支持庫,本文介紹 Python 的并行計算,,,從某種意義上說,這是一種世界觀,用來處理同時發生的多件事情,在21世紀,了解并行計算的來龍去脈能讓您獲益匪淺,
對并行計算表現能力最強大的可能就是 C 語言了[1],用 C 語言實作的并行計算,能夠真正挑戰計算機的物理極限,因為可以利用一些非常底層的系統呼叫,類似于 Linux 中的clone(),其實那就是用來實作執行緒的,有了這些利器,相當于掌控了整臺虛擬設備!
高級語言通常不會給出那么高的自由度,以便換取很多其他的便利,比如,普通的 PHP 根本不允許創建執行緒,并且在行程級別上干活的作業量會很大,謝天謝地,現在不是用 PHP 撰寫代碼,而是用 Python,它自帶了一套很有意思的并行計算體系,只要完全理解了這套體系,您就會成為全球排名前1%的 Python 程式員,
為了實作上述目標,有必要真正弄明白由現代作業系統提供給 Python 使用的并發原語(Primitive),理解了這一點,不僅會讓您成為更好的 Python 程式員,還將提升您這輩子所有語言的開發水平,
太酷了吧!興奮嗎?我就很興奮,那就開始吧!
(順便說一句,最讓人興奮的部分是深入探討何時執行緒不是真正的執行緒,您到時候自然就明白了,)
行程和執行緒(Processes and Threads)
就從基礎知識開始吧,現代作業系統為執行執行緒提供了兩種組織方式,行程就是一個正在運行的程式,執行緒是行程中的活動單位,這就為如何實作并行計算提供了兩種基本的選擇:N個行程或N個執行緒,
如果做些深入的了解,執行緒和行程確實有很多相似之處,其實在 Linux 中,之前提到的系統呼叫 clone() 既可以用于創建行程,也可以用來創建執行緒,只要呼叫函式時提供不同的引數即可,
在實操時,兩者的主要區別在于共享和不共享的東西不同,如果一個行程創建了兩個子行程,則每個子行程都擁有自己的記憶體,沒有什么共享的東西,(默認如此,有引數可以進行修改,)而新的執行緒不僅會共享其父行程的記憶體,還會共享檔案描述符、檔案系統的背景關系和信號處理程序,
采用多執行緒而不是多行程,有一些實實在在的好處,執行緒在記憶體占用方面要輕量得多,同樣是守護程式,相比生成10個執行緒而言,顯然生成10個不共享記憶體的子行程會占用更多的記憶體空間,此外,執行緒之間的通信和同步都比行程要簡單,根據定義,行程間的任何通信都要用到 IPC 呼叫,因此還會帶來切入內核態的開銷,當然可以在行程之間共享記憶體,但作業量要比執行緒多些[2],
執行緒的悲哀(The Tragedy of Threads)
簡而言之,在多執行緒和多行程這兩個并行模型中,理論上用執行緒可以寫出性能更高的應用程式,
哦哦,我說的只是“理論上”吧?沒錯,我們會被帶到溝里去:撰寫無錯的多執行緒代碼非常困難,您會遭遇各種微妙的、令人困惑的 Bug,想要很容易就重現這些 Bug 需要靠運氣,不走運的話就難了,競態條件(Race Condition)、死鎖(Deadlock)、活鎖(Live Lock)等等,還有很多,
解決這些問題的代價就是耗費開發時間,安全的執行緒編程涉及到規范地使用同步原語(Synchronization Primitive),諸如鎖和互斥鎖(Mutex),作為一名優秀的軟體工程師,這是應付不時之需的工具包(如果您還沒有的話),但不必這么做總還是最好的,
(等等,這是前兆嗎?我覺得是吧,,,)
此外還需要考慮一點,也是 Python 所特有的:Python 的執行緒并不完全像它看上去的那樣,
何時執行緒不是真正的執行緒(When Threads Aren't Really Threads)
上述執行緒實際上指的是作業系統執行緒(OS Thread),在用 C 語言撰寫程式時,呼叫pthread_create(OS X,Linux)或CreateThread(Windows)即可獲得這種執行緒,這是真正的執行緒,是由作業系統內核分配和管理的,但如果用 Python 這種高級語言創建執行緒,就不一定了,至少不完全是,
在較新版的 Python 中,只要線創建threading.Thread的實體,然后呼叫其start()方法即可啟動執行緒,已啟動的執行緒確實分到了一個獨立的作業系統執行緒,在大多數平臺上確實如此[3],兩者的區別在于:兩個作業系統執行緒可以同時運行,以充分利用各自獨立的核心或 CPU,但一般情況下兩個 Python 執行緒卻無法同時運行,
這是全域解釋器鎖導致的,也即 GIL,標準 Python 中存在一種機制,同時只允許1個執行緒運行 Python 位元組碼[4],即便是在128核的野獸級機器上運行,標準的多執行緒 Python 程式在任一時刻也只能用到其中1個核,[5]
乍一看這似乎很糟糕,但事實證明,對于大部分工程領域而言,GIL 根本就不算什么重大限制,就純粹的 CPU 性能而言[6],Python 的執行緒確實有些不如作業系統執行緒,不過 Python 行程完全不受 GIL 的影響,行程就是我們的出路,也是拯救處理器受限(CPU-Bound)任務的出路,第2部分將會討論行程,
其實匯編語言的表現能力更強,不過當前很少有人需要用到這么底層的東西了,在可能遭遇的情形下,C 的并行計算能力幾乎一樣強大, ??
在較新的 Linux 中,我知道的最佳方案是
mmap(),當然其他幾種機制也是可行的, ??某些內核不支持執行緒的作業系統除外,我希望您不必為此擔心, ??
引入 GIL 是一個非常好的決定,這樣解釋器的實作難度就降低了數量級,同時在通常的單執行緒情況下又維持住了性能,別忘了,Python 是由志愿者提供的, ??
其實這只說對了大約98%,解決方案有很多,具體取決于您愿意投入多大的精力,用 C 或 C++撰寫的擴展模塊可以臨時釋放 GIL,因此,像 numpy 這樣的軟體包并不受單核運行的限制,
而且,至少還有其他 Python 解釋器(Jython、IronPython、PyPy)具有實驗性的不帶 GIL 的分支(Branch)發布,不過對于您實際撰寫的純 Python 代碼,超過99%都只能完全用足一個 CPU 核, ??當然,那些并沒有接近 CPU 極限的執行緒,可以采用一些非常有用的編程模式,因此 GIL 甚至都無需多慮,建立一個回應式(Responsive)UI 就是個很好的例子:主執行緒可以迅速地賣力干活,而輔助執行緒則負責監測用戶的輸入、停止計算或執行其他操作, ??
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/282520.html
標籤:Python
下一篇:Python 極速入門指南
