多線程編程
其實創(chuàng)建線程之后,線程并不是始終保持一個狀態(tài)的,其狀態(tài)大概如下:
New 創(chuàng)建
Runnable 就緒。等待調(diào)度
Running 運行
Blocked 阻塞。阻塞可能在 Wait Locked Sleeping
Dead 消亡
線程有著不同的狀態(tài),也有不同的類型。大致可分為:
主線程
子線程
守護線程(后臺線程)
前臺線程
簡單了解完這些之后,我們開始看看具體的代碼使用了。
1、線程的創(chuàng)建
Python 提供兩個模塊進行多線程的操作,分別是 thread 和 threading
前者是比較低級的模塊,用于更底層的操作,一般應用級別的開發(fā)不常用。
#!/usr/bin/env python3 # -*- coding: UTF-8 -*- import time import threading class MyThread(threading.Thread): def run(self): for i in range(5): print('thread {}, @number: {}'.format(self.name, i)) time.sleep(1) def main(): print("Start main threading") # 創(chuàng)建三個線程 threads = [MyThread() for i in range(3)] # 啟動三個線程 for t in threads: t.start() print("End Main threading") if __name__ == '__main__': main()
運行結果:
Start main threading thread Thread-1, @number: 0 thread Thread-2, @number: 0 thread Thread-3, @number: 0 End Main threading thread Thread-2, @number: 1 thread Thread-1, @number: 1 thread Thread-3, @number: 1 thread Thread-1, @number: 2 thread Thread-3, @number: 2 thread Thread-2, @number: 2 thread Thread-2, @number: 3 thread Thread-3, @number: 3 thread Thread-1, @number: 3 thread Thread-3, @number: 4 thread Thread-2, @number: 4 thread Thread-1, @number: 4
注意喔,這里不同的環(huán)境輸出的結果肯定是不一樣的。
2、線程合并(join方法)
上面的示例打印出來的結果來看,主線程結束后,子線程還在運行。那么我們需要主線程要等待子線程運行完后,再退出,要怎么辦呢?
這時候,就需要用到 join 方法了。
在上面的例子,新增一段代碼,具體如下:
#!/usr/bin/env python3 # -*- coding: UTF-8 -*- import time import threading class MyThread(threading.Thread): def run(self): for i in range(5): print('thread {}, @number: {}'.format(self.name, i)) time.sleep(1) def main(): print("Start main threading") # 創(chuàng)建三個線程 threads = [MyThread() for i in range(3)] # 啟動三個線程 for t in threads: t.start() # 一次讓新創(chuàng)建的線程執(zhí)行 join for t in threads: t.join() print("End Main threading") if __name__ == '__main__': main()
從打印的結果,可以清楚看到,相比上面示例打印出來的結果,主線程是在等待子線程運行結束后才結束的。
Start main threading thread Thread-1, @number: 0 thread Thread-2, @number: 0 thread Thread-3, @number: 0 thread Thread-1, @number: 1 thread Thread-3, @number: 1 thread Thread-2, @number: 1 thread Thread-2, @number: 2 thread Thread-1, @number: 2 thread Thread-3, @number: 2 thread Thread-2, @number: 3 thread Thread-1, @number: 3 thread Thread-3, @number: 3 thread Thread-3, @number: 4 thread Thread-2, @number: 4 thread Thread-1, @number: 4 End Main threading
3、線程同步與互斥鎖
使用線程加載獲取數(shù)據(jù),通常都會造成數(shù)據(jù)不同步的情況。當然,這時候我們可以給資源進行加鎖,也就是訪問資源的線程需要獲得鎖才能訪問。
其中 threading 模塊給我們提供了一個 Lock 功能。
lock = threading.Lock()
在線程中獲取鎖
lock.acquire()
使用完成后,我們肯定需要釋放鎖
lock.release()
當然為了支持在同一線程中多次請求同一資源,Python 提供了可重入鎖(RLock)。RLock 內(nèi)部維護著一個 Lock 和一個 counter 變量,counter 記錄了 acquire 的次數(shù),從而使得資源可以被多次 require。直到一個線程所有的 acquire 都被 release,其他的線程才能獲得資源。
那么怎么創(chuàng)建重入鎖呢?也是一句代碼的事情:
r_lock = threading.RLock()
4、Condition 條件變量
實用鎖可以達到線程同步,但是在更復雜的環(huán)境,需要針對鎖進行一些條件判斷。Python 提供了 Condition 對象。使用 Condition 對象可以在某些事件觸發(fā)或者達到特定的條件后才處理數(shù)據(jù),Condition 除了具有 Lock 對象的 acquire 方法和 release 方法外,還提供了 wait 和 notify 方法。線程首先 acquire 一個條件變量鎖。如果條件不足,則該線程 wait,如果滿足就執(zhí)行線程,甚至可以 notify 其他線程。其他處于 wait 狀態(tài)的線程接到通知后會重新判斷條件。
其中條件變量可以看成不同的線程先后 acquire 獲得鎖,如果不滿足條件,可以理解為被扔到一個( Lock 或 RLock )的 waiting 池。直達其他線程 notify 之后再重新判斷條件。不斷的重復這一過程,從而解決復雜的同步問題。
該模式常用于生產(chǎn)者消費者模式,具體看看下面在線購物買家和賣家的示例:
#!/usr/bin/env python3 # -*- coding: UTF-8 -*- import threading, time class Consumer(threading.Thread): def __init__(self, cond, name): # 初始化 super(Consumer, self).__init__() self.cond = cond self.name = name def run(self): # 確保先運行Seeker中的方法 time.sleep(1) self.cond.acquire() print(self.name + ': 我這兩件商品一起買,可以便宜點嗎') self.cond.notify() self.cond.wait() print(self.name + ': 我已經(jīng)提交訂單了,你修改下價格') self.cond.notify() self.cond.wait() print(self.name + ': 收到,我支付成功了') self.cond.notify() self.cond.release() print(self.name + ': 等待收貨') class Producer(threading.Thread): def __init__(self, cond, name): super(Producer, self).__init__() self.cond = cond self.name = name def run(self): self.cond.acquire() # 釋放對瑣的占用,同時線程掛起在這里,直到被 notify 并重新占有瑣。 self.cond.wait() print(self.name + ': 可以的,你提交訂單吧') self.cond.notify() self.cond.wait() print(self.name + ': 好了,已經(jīng)修改了') self.cond.notify() self.cond.wait() print(self.name + ': 嗯,收款成功,馬上給你發(fā)貨') self.cond.release() print(self.name + ': 發(fā)貨商品') cond = threading.Condition() consumer = Consumer(cond, '買家(兩點水)') producer = Producer(cond, '賣家(三點水)') consumer.start() producer.start()
輸出的結果如下:
買家(兩點水): 我這兩件商品一起買,可以便宜點嗎 賣家(三點水): 可以的,你提交訂單吧 買家(兩點水): 我已經(jīng)提交訂單了,你修改下價格 賣家(三點水): 好了,已經(jīng)修改了 買家(兩點水): 收到,我支付成功了 買家(兩點水): 等待收貨 賣家(三點水): 嗯,收款成功,馬上給你發(fā)貨 賣家(三點水): 發(fā)貨商品
5、線程間通信
如果程序中有多個線程,這些線程避免不了需要相互通信的。那么我們怎樣在這些線程之間安全地交換信息或數(shù)據(jù)呢?
從一個線程向另一個線程發(fā)送數(shù)據(jù)最安全的方式可能就是使用 queue 庫中的隊列了。創(chuàng)建一個被多個線程共享的 Queue 對象,這些線程通過使用 put() 和 get() 操作來向隊列中添加或者刪除元素。
# -*- coding: UTF-8 -*- from queue import Queue from threading import Thread isRead = True def write(q): # 寫數(shù)據(jù)進程 for value in ['兩點水', '三點水', '四點水']: print('寫進 Queue 的值為:{0}'.format(value)) q.put(value) def read(q): # 讀取數(shù)據(jù)進程 while isRead: value = q.get(True) print('從 Queue 讀取的值為:{0}'.format(value)) if __name__ == '__main__': q = Queue() t1 = Thread(target=write, args=(q,)) t2 = Thread(target=read, args=(q,)) t1.start() t2.start()
輸出的結果如下:
寫進 Queue 的值為:兩點水 寫進 Queue 的值為:三點水 從 Queue 讀取的值為:兩點水 寫進 Queue 的值為:四點水 從 Queue 讀取的值為:三點水 從 Queue 讀取的值為:四點水
Python 還提供了 Event 對象用于線程間通信,它是由線程設置的信號標志,如果信號標志位真,則其他線程等待直到信號接觸。
Event 對象實現(xiàn)了簡單的線程通信機制,它提供了設置信號,清楚信號,等待等用于實現(xiàn)線程間的通信。
設置信號
使用 Event 的 set() 方法可以設置 Event 對象內(nèi)部的信號標志為真。Event 對象提供了 isSe() 方法來判斷其內(nèi)部信號標志的狀態(tài)。當使用 event 對象的 set() 方法后,isSet() 方法返回真
清除信號
使用 Event 對象的 clear() 方法可以清除 Event 對象內(nèi)部的信號標志,即將其設為假,當使用 Event 的 clear 方法后,isSet() 方法返回假
等待
Event 對象 wait 的方法只有在內(nèi)部信號為真的時候才會很快的執(zhí)行并完成返回。當 Event 對象的內(nèi)部信號標志位假時,則 wait 方法一直等待到其為真時才返回。
示例:
# -*- coding: UTF-8 -*- import threading class mThread(threading.Thread): def __init__(self, threadname): threading.Thread.__init__(self, name=threadname) def run(self): # 使用全局Event對象 global event # 判斷Event對象內(nèi)部信號標志 if event.isSet(): event.clear() event.wait() print(self.getName()) else: print(self.getName()) # 設置Event對象內(nèi)部信號標志 event.set() # 生成Event對象 event = threading.Event() # 設置Event對象內(nèi)部信號標志 event.set() t1 = [] for i in range(10): t = mThread(str(i)) # 生成線程列表 t1.append(t) for i in t1: # 運行線程 i.start()
輸出的結果如下:
1 0 3 2 5 4 7 6 9 8
6、后臺線程
默認情況下,主線程退出之后,即使子線程沒有 join。那么主線程結束后,子線程也依然會繼續(xù)執(zhí)行。如果希望主線程退出后,其子線程也退出而不再執(zhí)行,則需要設置子線程為后臺線程。Python 提供了 setDeamon 方法。