python多線程編程筆記

多線程編程

多線程編程對於具有如下特點的編程任務而言是非常理想的:本質上是異步的;需要多個併發活動;每個活動的處理順序可能是不確定的,或者說隨機的、不可預測的。這種編程任務可以被組織或劃分成多個執行流,其中每個執行流都有一個指定要完成的任務。根據應用的不同,這些子任務可能需要計算出中間結果,然後合併爲最終的輸出結果。
計算密集型的任務可以比較容易地劃分成多個子任務,然後按順序執行或按照多線程方式執行。而那種使用單線程處理多個外部輸入源的任務就不那麼簡單了。如果不使用多線程,要實現這種編程任務就需要爲串行程序使用一個或多個計時器,並實現一個多路複用方案。

進程:進程(有時稱爲重量級進程)則是一個執行中的程序。每個進程都擁有自己的地址空間、內存、數據棧以及其他用於跟蹤執行的輔助數據。操作系統管理其上所有進程的執行,併爲這些進程合理地分配時間。進程也可以通過派生(fork 或 spawn)新的進程來執行其他任務,不過因爲每個新進程也都擁有自己的內存和數據棧等,所以只能採用進程間通信(IPC)的方式共享信息。

線程:線程(有時候稱爲輕量級進程)與進程類似,不過它們是在同一個進程下執行的,並共享相同的上下文 線程包括開始、執行順序和結束三部分。它有一個指令指針,用於記錄當前運行的上下文。當其他線程運行時,它可以被搶佔(中斷)和臨時掛起(也稱爲睡眠) ——這種做法叫做讓步(yielding)。
一個進程中的各個線程與主線程共享同一片數據空間,因此相比於獨立的進程而言,線
程間的信息共享和通信更加容易。線程一般是以併發方式執行的,正是由於這種並行和數據共享機制,使得多任務間的協作成爲可能。當然,在單核 CPU 系統中,因爲真正的併發是不可能的,所以線程的執行實際上是這樣規劃的:每個線程運行一小會兒,然後讓步給其他線程(再次排隊等待更多的 CPU 時間)。在整個進程的執行過程中,每個線程執行它自己特定的任務,在必要時和其他線程進行結果通信。

全局解釋器鎖
Python 代碼的執行是由 Python 虛擬機(又名解釋器主循環)進行控制的。 Python 在
設計時是這樣考慮的,在主循環中同時只能有一個控制線程在執行,就像單核 CPU 系統中的多進程一樣。內存中可以有許多程序,但是在任意給定時刻只能有一個程序在運行。同理,儘管 Python 解釋器中可以運行多個線程,但是在任意給定時刻只有一個線程會被解釋器執行。
對 Python 虛擬機的訪問是由全局解釋器鎖(GIL)控制的。這個鎖就是用來保證同時只能有一個線程運行的。在多線程環境中, Python 虛擬機將按照下面所述的方式執行。
1.設置 GIL。
2.切換進一個線程去運行。
3.執行下面操作之一。
a.指定數量的字節碼指令。
b.線程主動讓出控制權(可以調用 time.sleep(0)來完成)。
4.把線程設置回睡眠狀態(切換出線程)。
5.解鎖 GIL。
6.重複上述步驟。
例如,對於任意麪向 I/O 的 Python 例程(調用了內置的操作系統 C 代碼的那種),
GIL 會在 I/O 調用前被釋放,以允許其他線程在 I/O 執行的時候運行。而對於那些沒有太多 I/O 操作的代碼而言,更傾向於在該線程整個時間片內始終佔有處理器(和 GIL)。換句話說就是, I/O 密集型的 Python 程序要比計算密集型的代碼能夠更好地利用多線程環境。

Python的threaing模塊
Python 提供了多個模塊來支持多線程編程,包括 thread、 threading 和 Queue 模塊等。程序是可以使用 thread 和 threading 模塊來創建與管理線程。 thread 模塊提供了基本的線程和鎖定支持;而 threading 模塊提供了更高級別、功能更全面的線程管理。使用 Queue 模塊,用戶可以創建一個隊列數據結構,用於在多線程之間進行共享。
避免使用 thread 模塊:最明顯的一個原因是在主線程退出之後,所有其他線程都會在沒有清理的情況下直接退出(不支持守護線程)。低級別的 thread 模塊擁有的同步原語很少(實際上只有一個),而另一個模塊 threading 會確保在所有“重要的”子線程退出前,保持整個進程的存活。 只建議那些想訪問線程的更底層級別的專家使用 thread 模塊。爲了強調這一點,在 Python3 中該模塊被重命名爲_thread。
在這裏插入圖片描述
如果主線程準備退出時,不需要等待某些子線程完成,就可以爲這些子線程設置守護
線程標記。該標記值爲真時,表示該線程是不重要的,或者說該線程只是用來等待客戶端
請求而不做任何其他事情。
要將一個線程設置爲守護線程,需要在啓動線程之前執行如下賦值語句:thread.daemon = True(調用 thread.setDaemon(True)的舊方法已經棄用了)。同樣,要檢查線程的守護狀態,也只需要檢查這個值即可(對比過去調用 thread.isDaemon()的方法)。一個新的子線程會繼承父線程的守護標記。
Threading有三種用法:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
使用 thread 模塊時實現的鎖沒有了,取而代之的是一組 Thread 對象。當實例化每個 Thread 對象時,把函數(target)和參數(args)傳進去,然後得到返回的 Thread 實例。實例化 Thread(調用 Thread())和調用 thread.start_new_thread()的最大區別是新線程不會立即開始執行。這是一個非常有用的同步功能,尤其是當你並不希望線程立即開始執行時。
當所有線程都分配完成之後,通過調用每個線程的 start()方法讓它們開始執行,而不是在這之前就會執行。相比於管理一組鎖(分配、獲取、釋放、檢查鎖狀態等)而言,這裏只需要爲每個線程調用 join()方法即可。 join()方法將等待線程結束,或者在提供了超時時間的情況下,達到超時時間。使用 join()方法要比等待鎖釋放的無限循環更加清晰(這也是這種鎖
又稱爲自旋鎖的原因)。
在這裏插入圖片描述
在這裏插入圖片描述
主要是添加了 ThreadFunc 類,並在實例化 Thread 對象時做了一點小改動,同時實例化了可調用類 ThreadFunc。實際上,這裏完成了兩個實例化。讓我們先仔細看看 ThreadFunc 類吧。我們希望這個類更加通用,而不是侷限於 loop()函數,因此添加了一些新的東西,比如讓這個類保存了函數的參數、函數自身以及函數名的字符串。而構造函數__init__()用於設定上述這些值。當創建新線程時, Thread 類的代碼將調用 ThreadFunc 對象,此時會調用__call__()這個特殊方法。由於我們已經有了要用到的參數,這裏就不需要再將其傳遞給 Thread()的構造函數了,直接調用即可。
在這裏插入圖片描述

同步原語
前面對同步進行過一些介紹,所以這裏就使用其中兩種類型的同步原語演示幾個示例程
序:鎖/互斥,以及信號量。鎖是所有機制中最簡單、最低級的機制,而信號量用於多線程競爭有限資源的情況。
鎖有兩種狀態:鎖定和未鎖定。而且它也只支持兩個函數:獲得鎖和釋放鎖。當多線程爭奪鎖時,允許第一個獲得鎖的線程進入臨界區,並執行代碼。所有之後到達的線程將被阻塞,直到第一個線程執行結束,退出臨界區,並釋放鎖。此時,其他等待的線程可以獲得鎖並進入臨界區。不過請記住,那些被阻塞的線程是沒有順序的(即不是先到先執行),勝出線程的選擇是不確定的,而且還會根據 Python 實現的不同而有所區別。

使用上下文管理:
使用 Python 2.5 或更新版本,還有一種方案可以不再調用鎖的 acquire()和 release()
方法,從而更進一步簡化代碼。這就是使用 with 語句,此時每個對象的上下文管理器負責在進入該套件之前調用 acquire()並在完成執行之後調用 release()。
在這裏插入圖片描述在這裏插入圖片描述

信號量
信號量是最古老的同步原語之一。它是一個計數器,當資源消耗時遞減,當資源釋放時遞增。你可以認爲信號量代表它們的資源可用或不可用。消耗資源使計數器遞減的操作
習慣上稱爲 P() (來源於荷蘭單詞 probeer/proberen),也稱爲 wait、try、acquire、pend 或 procure。相對地,當一個線程對一個資源完成操作時,該資源需要返回資源池中。這個操作一般稱爲 V()(來源於荷蘭單詞 verhogen/verhoog),也稱爲 signal、 increment、 release、 post、 vacate。Python 簡化了所有的命名,使用和鎖的函數/方法一樣的名字: acquire 和 release。信號量比鎖更加靈活,因爲可以有多個線程,每個線程擁有有限資源的一個實例 。

生產者-消費者問題和Queue/queue模塊

多線程常用的第三方庫
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章