當前位置:
首頁 > 最新 > 十五分鐘了解 Python 並發編程

十五分鐘了解 Python 並發編程

Python 的並發編程

在 Python 中並發編程是一件非常有趣的事情,這篇文章將講解 Python 並發編程的基本操作。並發和並行是對孿生兄弟,概念經常混淆。並發是指能夠多任務處理,並行則是是能夠同時多任務處理。Erlang 之父 Joe Armstrong 有一張非常有趣的圖說明這兩個概念:

我個人更喜歡的一種說法是:並發是宏觀並行而微觀串列。

雖然 Python 自帶了很好的類庫支持多線程 / 進程編程,但眾所周知,因為 GIL 的存在,Python 很難做好真正的並行。

GIL 指全局解釋器鎖,對於 GIL 的介紹:

全局解釋器鎖(英語:Global Interpreter Lock,縮寫 GIL),是計算機程序設計語言解釋器用於同步線程的一種機制,它使得任何時刻僅有一個線程在執行。

維基百科

其實與其說 GIL 是 Python 解釋器的限制,不如說是 CPython 的限制,因為 Python 為了保障性能,底層大多使用 C 實現的,而 CPython 的內存管理並不是線程安全的,為了保障整體的線程安全,解釋器便禁止多線程的並行執行。

因為 Python 社區認為操作系統的線程調度已經非常成熟了,沒有必要自己再實現一遍,因此 Python 的線程切換基本是依賴操作系統,在實際的使用中,對於單核 CPU,GIL 並沒有太大的影響,但對於多核 CPU 卻引入了線程顛簸(thrashing)問題。

線程顛簸是指作為單一資源的 GIL 鎖,在被多核心競爭強佔時資源額外消耗的現象。

比如下圖,線程 1 在釋放 GIL 鎖後,操作系統喚醒了 線程 2,並將 線程 2 分配給 核心 2 執行,但是如果此時 線程 2 卻沒有成功獲得 GIL 鎖,只能再次被掛起。此時切換線程、切換上下文的資源都將白白浪費。

因此,Python 多線程程序在多核 CPU 機器下的性能不一定比單核高。那麼如果是計算密集型的程序,一般還是考慮用 C 重寫關鍵部分,或者使用多進程避開 GIL。

在 Python 中使用多線程,有 和 可供原則, 提供了低級別的、原始的線程以及一個簡單的鎖,因為 過於簡陋,線程管理容易出現人為失誤,因此官方更建議使用 ,而 也不過是對 的封裝和補充。(Python3 中 被改名為 )。

在 Python 中創建線程非常簡單:

直接創建線程簡單優雅,如果邏輯複雜,也可以通過繼承 Thread 基類完成多線程:


在 Python 中,可以使用 庫來實現多進程編程,和多線程一樣,有兩種方法可以使用多進程編程。

直接創建進程:

繼承進程父類:

除了常用的多進程編程外,我認為它最大的意義在於提供了一套規範,在該庫下有一個 模塊,即 ,裡面對 進行封裝,提供了和 相同 API 的線程實現,換句話說, 提供的是進程任務類,而 ,也正是有 的存在,可以快速的講一個多進程程序改為多線程:

無論是多線程還是多進程編程,這也是我一般會選擇 的原因。

除了直接創建進程,還可以用進程池(或者 里的進程池):

線程池:

這裡示例有個問題,pool 在 join 前需要 close 掉,否則就會拋出異常,不過 Python 之禪的作者 Tim Peters 給出解釋:

As to Pool.close(), you should call that when - and only when - you"re never going to submit more work to the Pool instance. So Pool.close() is typically called when the parallelizable part of your main program is finished. Then the worker processes will terminate when all work already assigned has completed.

It"s also excellent practice to call Pool.join() to wait for the worker processes to terminate. Among other reasons, there"s often no good way to report exceptions in parallelized code (exceptions occur in a context only vaguely related to what your main program is doing), and Pool.join() provides a synchronization point that can report some exceptions that occurred in worker processes that you"d otherwise never see.

在多進程編程中,因為進程間的資源隔離,不需要考慮內存的線程安全問題,而在多線程編程中便需要同步原語來保存線程安全,因為 Python 是一門簡單的語言,很多操作都是封裝的操作系統 API,因此支持的同步原語蠻全,但這裡只寫兩種常見的同步原語:鎖和信號量。

通過使用鎖可以用來保護一段內存空間,而信號量可以被多個線程共享。

在 中可以看到 鎖和 重用鎖兩種鎖,區別如名。這兩種鎖都只能被一個線程擁有,第一種鎖只能被獲得一次,而重用鎖可以被多次獲得,但也需要同樣次數的釋放才能真正的釋放。

當多個線程對同一塊內存空間同時進行修改的時候,經常遇到奇怪的問題:

如上就是典型的非線程安全導致 count 沒有達到預期的效果。而通過鎖便可以控制某一段代碼,或者說某段內存空間的訪問:

當然,上述例子非常暴力,直接強行把並發改為串列。

對於信號量常見於有限資源強佔的場景,可以定義固定大小的信號量供多個線程獲取或者釋放,從而控制線程的任務執行,比如下面的例子,控制最多有 5 個任務在執行:

因為多進程的內存隔離,不會存在內存競爭的問題。但同時,多個進程間的數據共享成為了新的問題,而進程間通信常見:隊列,管道,信號。

這裡只講解隊列和管道。

隊列常見於雙進程模型,一般用作生產者 - 消費者模式,由生產者進程向隊列中發布任務,並由消費者從隊列首部拿出任務進行執行:

理論上每個進程都可以向隊列里的讀或者寫,可以認為隊列是半雙工路線。但是往往只有特定的讀進程(比如消費者)和寫進程(比如生產者),儘管這些進程只是開發者自己定義的。

而 Pipe 更像一個全工路線:


除了上面介紹的 和 兩個庫外,還有一個好用的令人髮指的庫 。和前面兩個庫不同,這個庫是更高等級的抽象,隱藏了很多底層的東西,但也因此非常好用。用官方的例子:

該庫中自帶了進程池和線程池,可以通過上下文管理器來管理,而且對於非同步任務執行完後,結果的獲得也非常簡單。再拿一個官方的多進程計算的例子作為結束:


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Python 的精彩文章:

2018你該認真學Python了
Python商務辦公——python+pandas高效實現Excel文件合併與分析

TAG:Python |