Python這門解釋性語言也有專門的線程模型,Python虛擬機使用GIL(Global Interpreter Lock,全局解釋器鎖)來互斥線程對共享資源的訪問,但暫時無法利用多處理器的優勢。
在Python中我們主要是通過thread和 threading這兩個模塊來實現的,其中Python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用,所以我們使用 threading模塊實現多線程編程。這篇文章我們主要來看看Python對多線程編程的支持。
在語言層面,Python對多線程提供了很好的支持,可以方便地支持創建線程、互斥鎖、信號量、同步等特性。下面就是官網上介紹threading模塊的基本資料及功能:
實現模塊
- thread:多線程的底層支持模塊,一般不建議使用;
- threading:對thread進行了封裝,將一些線程的操作對象化
threading模塊
- Thread 線程類,這是我們用的最多的一個類,你可以指定線程函數執行或者繼承自它都可以實現子線程功能;
- Timer與Thread類似,但要等待一段時間後纔開始運行;
- Lock 鎖原語,這個我們可以對全局變量互斥時使用;
- RLock 可重入鎖,使單線程可以再次獲得已經獲得的鎖;
- Condition 條件變量,能讓一個線程停下來,等待其他線程滿足某個“條件”;
- Event 通用的條件變量。多個線程可以等待某個事件發生,在事件發生後,所有的線程都被激活;
- Semaphore爲等待鎖的線程提供一個類似“等候室”的結構;
- BoundedSemaphore 與semaphore類似,但不允許超過初始值;
- Queue:實現了多生產者(Producer)、多消費者(Consumer)的隊列,支持鎖原語,能夠在多個線程之間提供很好的同步支持。
其中Thread類
- 是你主要的線程類,可以創建進程實例。該類提供的函數包括:
- getName(self) 返回線程的名字
- isAlive(self) 布爾標誌,表示這個線程是否還在運行中
- isDaemon(self) 返回線程的daemon標誌
- join(self, timeout=None) 程序掛起,直到線程結束,如果給出timeout,則最多阻塞timeout秒
- run(self) 定義線程的功能函數
- setDaemon(self, daemonic) 把線程的daemon標誌設爲daemonic
- setName(self, name) 設置線程的名字
- start(self) 開始線程執行
其中Queue提供的類
- Queue隊列
- LifoQueue後入先出(LIFO)隊列
- PriorityQueue 優先隊列
接下來,我們將會用一個一個示例來展示threading的各個功能,包括但不限於:兩種方式起線程、threading.Thread類的重要函數、使用Lock互斥及RLock實現重入鎖、使用Condition實現生產者和消費者模型、使用Event和Semaphore多線程通信
在Python中我們主要是通過thread和threading這兩個模塊來實現的,其中Python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用,所以我們使用threading模塊實現多線程編程。一般來說,使用線程有兩種模式,一種是創建線程要執行的函數,把這個函數傳遞進Thread對象裏,讓它來執行;另一種是直接從Thread繼承,創建一個新的class,把線程執行的代碼放到這個新的 class裏。
將函數傳遞進Thread對象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
程序啓動了3個線程,並且打印了每一個線程的線程名字,這個比較簡單吧,處理重複任務就派出用場了,下面介紹使用繼承threading的方式;
繼承自threading.Thread類:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
接下來,將會介紹如何控制這些線程,包括子線程的退出,子線程是否存活及將子線程設置爲守護線程(Daemon)。
介紹threading模塊中的主類Thread的一些主要方法,實例代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1、name相關
你可以爲每一個thread指定name,默認的是Thread-No形式的,如上述實例代碼打印出的一樣:
I am Thread-1 I am Thread-2 I am Thread-3 I am Thread-4 I am Thread-5
當然你可以指定每一個thread的name,這個通過setName方法,代碼:
1 2 3 |
|
2、join方法
join方法原型如下,這個方法是用來阻塞當前上下文,直至該線程運行結束:
1 |
|
timeout可以設置超時時間
3、setDaemon方法
當我們在程序運行中,執行一個主線程,如果主線程又創建一個子線程,主線程和子線程就分兵兩路,當主線程完成想退出時,會檢驗子線程是否完成。如果子線程未完成,則主線程會等待子線程完成後再退出。但是有時候我們需要的是,只要主線程完成了,不管子線程是否完成,都要和主線程一起退出,這時就可以用setDaemon方法,並設置其參數爲True。
現在我們考慮這樣一個問題:假設各個線程需要訪問同一公共資源,我們的代碼該怎麼寫?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
解決上面的問題,我們興許會寫出這樣的代碼,我們假設跑200個線程,但是這200個線程都會去訪問counter這個公共資源,並對該資源進行處理(counter += 1),代碼看起來就是這個樣了,但是我們看下運行結果:
I am Thread-69, set counter:64 I am Thread-73, set counter:66I am Thread-74, set counter:67I am Thread-75, set counter:68I am Thread-76, set counter:69I am Thread-78, set counter:70I am Thread-77, set counter:71I am Thread-58, set counter:72I am Thread-60, set counter:73I am Thread-62, set counter:74I am Thread-66, set counter:75I am Thread-70, set counter:76I am Thread-72, set counter:77I am Thread-79, set counter:78I am Thread-71, set counter:78
打印結果我只貼了一部分,從中我們已經看出了這個全局資源(counter)被搶佔的情況,問題產生的原因就是沒有控制多個線程對同一資源的訪問,對數據造成破壞,使得線程運行的結果不可預期。這種現象稱爲“線程不安全”。在開發過程中我們必須要避免這種情況,那怎麼避免?這就用到了我們在綜述中提到的互斥鎖了。
互斥鎖概念
Python編程中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性。每個對象都對應於一個可稱爲” 互斥鎖” 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。在Python中我們使用threading模塊提供的Lock類。
我們對上面的程序進行整改,爲此我們需要添加一個互斥鎖變量mutex = threading.Lock(),然後在爭奪資源的時候之前我們會先搶佔這把鎖mutex.acquire(),對資源使用完成之後我們在釋放這把鎖mutex.release()。代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
同步阻塞
當一個線程調用Lock對象的acquire()方法獲得鎖時,這把鎖就進入“locked”狀態。因爲每次只有一個線程1可以獲得鎖,所以如果此時另一個線程2試圖獲得這個鎖,該線程2就會變爲“block“同步阻塞狀態。直到擁有鎖的線程1調用鎖的release()方法釋放鎖之後,該鎖進入“unlocked”狀態。線程調度程序從處於同步阻塞狀態的線程中選擇一個來獲得鎖,並使得該線程進入運行(running)狀態。
進一步考慮
通過對公共資源使用互斥鎖,這樣就簡單的到達了我們的目的,但是如果我們又遇到下面的情況:
- 1、遇到鎖嵌套的情況該怎麼辦,這個嵌套是指當我一個線程在獲取臨界資源時,又需要再次獲取;
- 2、如果有多個公共資源,在線程間共享多個資源的時候,如果兩個線程分別佔有一部分資源並且同時等待對方的資源;
上述這兩種情況會直接造成程序掛起,即死鎖,下面我們會談死鎖及可重入鎖RLock。
前一篇文章Python:使用threading模塊實現多線程編程四[使用Lock互斥鎖]我們已經開始涉及到如何使用互斥鎖來保護我們的公共資源了,現在考慮下面的情況–
如果有多個公共資源,在線程間共享多個資源的時候,如果兩個線程分別佔有一部分資源並且同時等待對方的資源,這會引起什麼問題?
死鎖概念
所謂死鎖: 是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。 由於資源佔用是互斥的,當某個進程提出申請資源後,使得有關進程在無外力協助下,永遠分配不到必需的資源而無法繼續運行,這就產生了一種特殊現象死鎖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
代碼中展示了一個線程的兩個功能函數分別在獲取了一個競爭資源之後再次獲取另外的競爭資源,我們看運行結果:
I am Thread-1 , get res: ResA I am Thread-1 , get res: ResB I am Thread-2 , get res: ResAI am Thread-1 , get res: ResB
可以看到,程序已經掛起在那兒了,這種現象我們就稱之爲”死鎖“。
避免死鎖
避免死鎖主要方法就是:正確有序的分配資源,避免死鎖算法中最有代表性的算法是Dijkstra E.W 於1968年提出的銀行家算法。
考慮這種情況:如果一個線程遇到鎖嵌套的情況該怎麼辦,這個嵌套是指當我一個線程在獲取臨界資源時,又需要再次獲取。
根據這種情況,代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
這種情況的代碼運行情況如下:
I am Thread-1, set counter:1
之後就直接掛起了,這種情況形成了最簡單的死鎖。
那有沒有一種情況可以在某一個線程使用互斥鎖訪問某一個競爭資源時,可以再次獲取呢?在Python中爲了支持在同一線程中多次請求同一資源,python提供了“可重入鎖”:threading.RLock。這個RLock內部維護着一個Lock和一個counter變量,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個線程所有的acquire都被release,其他的線程才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖:
代碼只需將上述的:
1 |
|
替換成:
1 |
|
目前我們已經會使用Lock去對公共資源進行互斥訪問了,也探討了同一線程可以使用RLock去重入鎖,但是儘管如此我們只不過才處理了一些程序中簡單的同步現象,我們甚至還不能很合理的去解決使用Lock鎖帶來的死鎖問題。所以我們得學會使用更深層的解決同步問題。
Python提供的Condition對象提供了對複雜線程同步問題的支持。Condition被稱爲條件變量,除了提供與Lock類似的acquire和release方法外,還提供了wait和notify方法。
使用Condition的主要方式爲:線程首先acquire一個條件變量,然後判斷一些條件。如果條件不滿足則wait;如果條件滿足,進行一些處理改變條件後,通過notify方法通知其他線程,其他處於wait狀態的線程接到通知後會重新判斷條件。不斷的重複這一過程,從而解決複雜的同步問題。
下面我們通過很著名的“生產者-消費者”模型來來演示下,在Python中使用Condition實現複雜同步。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
代碼中主要實現了生產者和消費者線程,雙方將會圍繞products來產生同步問題,首先是2個生成者生產products ,而接下來的10個消費者將會消耗products,代碼運行如下:
Producer(Thread-1):deliver one, now products:1 Producer(Thread-2):deliver one, now products:2 Consumer(Thread-3):consume one, now products:1 Consumer(Thread-4):only 1, stop consume, products:1 Consumer(Thread-5):only 1, stop consume, products:1 Consumer(Thread-6):only 1, stop consume, products:1 Consumer(Thread-7):only 1, stop consume, products:1 Consumer(Thread-8):only 1, stop consume, products:1 Consumer(Thread-10):only 1, stop consume, products:1 Consumer(Thread-9):only 1, stop consume, products:1 Consumer(Thread-12):only 1, stop consume, products:1 Consumer(Thread-11):only 1, stop consume, products:1
另外:Condition對象的構造函數可以接受一個Lock/RLock對象作爲參數,如果沒有指定,則Condition對象會在內部自行創建一個RLock;除了notify方法外,Condition對象還提供了notifyAll方法,可以通知waiting池中的所有線程嘗試acquire內部鎖。由於上述機制,處於waiting狀態的線程只能通過notify方法喚醒,所以notifyAll的作用在於防止有線程永遠處於沉默狀態。
使用threading.Event可以實現線程間相互通信,之前的Python:使用threading模塊實現多線程編程七[使用Condition實現複雜同步]我們已經初步實現了線程間通信的基本功能,但是更爲通用的一種做法是使用threading.Event對象。
使用threading.Event可以使一個線程等待其他線程的通知,我們把這個Event傳遞到線程對象中,Event默認內置了一個標誌,初始值爲False。一旦該線程通過wait()方法進入等待狀態,直到另一個線程調用該Event的set()方法將內置標誌設置爲True時,該Event會通知所有等待狀態的線程恢復運行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
運行效果如下:
I am Thread-1,I will sleep ... I am Thread-2,I will sleep ... I am Thread-3,I will sleep ... main thread sleep 3 seconds... I am Thread-1, I awake...I am Thread-2, I awake... I am Thread-3, I awake...
來自:http://www.ourunix.org/
http://xlambda.com/gevent-tutorial/