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