同步原語
. 一般在多線程的編程中,總會有特定的函數或模塊不希望被多個線程同時執行,比如修改數據庫、更新文件或是其它會產生競態條件的類似情況。而且如果兩個線程的運行順序發生變化可能導致代碼的執行軌跡或行爲不相同,或者產生不一樣的數據。
這就是需要同步的情況。當任意數量的線程可以訪問臨界區的資源,但是在給定時刻只有一個線程可以通過時,就是使用同步的時候了。這裏介紹兩種類型的同步原語——鎖/互斥和信號量。
鎖示例
. 鎖有兩種狀態:鎖定和未鎖定。而且它也只支持兩個函數:獲得鎖和釋放鎖。當多線程爭奪鎖時,允許第一個獲得鎖的線程進入臨界區,並執行代碼。所有之後到達的線程將被阻塞,直到第一個線程執行結束,退出臨界區,並釋放鎖。此時,其他等待的線程可以獲得鎖並進入臨界區。不過請記住,那些被阻塞的線程是沒有順序的(即不是先到先執行),勝出線程的選擇是不確定的,而且還會根據 Python 實現的不同而有所區別。
下面的示例創建多個線程併發執行,每個線程的功能只是睡眠一段時間:
from atexit import register
from random import randrange
from threading import Thread,Lock,current_thread
from time import ctime,sleep
class CleanOutputSet(set):
def __str__(self):
return ','.join(x for x in self)
lock = Lock()
#隨機創建3-6個線程,每個線程隨機隨眠2-4秒
loops = (randrange(2,5) for x in range(randrange(3,7)))
remaining = CleanOutputSet()
def loop(nsec):
myname = current_thread().name
lock.acquire()
remaining.add(myname) #將執行的線程加入set
print('{0} started {1}'.format(ctime(),myname))
lock.release()
sleep(nsec)
lock.acquire()
remaining.remove(myname) #將執行的線程移出set
print('{0} completed {1} ({2} secs)'.format(ctime(),myname,nsec))
print(' (remaining:{0})'.format(remaining or 'NONE'))
lock.release()
def main():
for pause in loops:
Thread(target=loop,args=(pause,)).start()
@register #這個不能忽略
def _atexit():
print('all done at:',ctime())
if __name__ == '__main__':
main()
. 代碼中創建的類是set的子類,包括一個對__str__()的實現,將輸出改爲所有元素以逗號隔開。
這裏使用 atexit.register()函數來告知腳本何時結束,這個函數會在Python解釋器中註冊一個退出函數,即它會在腳本退出前請求調用這個特殊函數。
使用上下文進行管理
. 還有一種方案可以不適用鎖的acquire()和release()方法,從而更進一步簡化代碼。就是使用with語句,此時每個對象的上下文管理器負責在進入該套件之前調用acquire()並在執行完成後調用release()。
with語句適用於資源訪問的場合,比如文件使用後自動關閉/線程中鎖的自動獲取和釋放等。threading 模塊的對象 Lock、RLock、Condition、Semaphore 和 BoundedSemaphore 都包含上下文管理器,也就是說,他們都可以使用with語句來簡化代碼,之前的loop函數可以寫爲:
def loop(nsec):
myname = current_thread().name
with lock:
remaining.add(myname)
print('{0} started {1}'.format(ctime(),myname))
sleep(nsec)
with lock:
remaining.remove(myname)
print('{0} completed {1} ({2} secs)'.format(ctime(),myname,nsec))
print(' (remaining:{0})'.format(remaining or 'NONE'))
信號量示例
. 鎖易於理解和實現,但是在情況更加複雜的情況下,可能還需要更強大的同步原語來代替鎖。對於擁有有限資源的應用來說,使用信號量可能是個不錯的選擇。
信號量是最古老的同步原語之一。 它是一個計數器,當資源消耗時遞減,在資源釋放時遞增。消耗資源使計數器遞減的操作習慣上稱爲P() (來源於荷蘭單詞probeer/proberen),也稱爲wait、try、acquire、pend或procure。當一個線程對一個資源完成操作時,該資源需要返回資源池中。這個操作一般稱爲 V()(來源於荷蘭單詞 verhogen/verhoog),也稱爲 signal、increment、release、post、vacate。
Python 簡化了所有的命名,使用和鎖的函數/方法一樣的名字:acquire 和 release。 信號量比鎖更加靈活,因爲可以有多個線程,每個線程擁有有限資源的一個實例。
下面的例子模擬一個簡化的糖果機,這個機器有5個槽來存儲糖果,當槽滿的時候無法再添加糖果,槽空的時候消費者無法購買糖果。可以使用信號量來追蹤這些有限的資源(糖果槽):
from atexit import register
from random import randrange
from threading import Thread,Lock,BoundedSemaphore
from time import ctime,sleep
lock = Lock()
MAX = 5
#BoundedSemaphore 的一個額外功能是這個計數器的值永遠不會超過它的初始值,換
#句話說,它可以防範其中信號量釋放次數多於獲得次數的異常用例。
candytray = BoundedSemaphore(MAX)
def refill():
lock.acquire()
print('refilling candy...')
try:
candytray.release()
except ValueError:
print('Full,skipping...')
else:
print('ok')
lock.release()
def buy():
lock.acquire()
print('Buy candy...')
#計數器的值不能小於 0,因此這個調用一般會在計數器再次增加之前被阻塞。
#通過傳入非阻塞的標誌 False,讓調用不再阻塞,而在應當阻塞的時候返回一個 False,
#指明沒有更多的資源了。
if candytray.acquire(False):
print('ok')
else:
print('empty,skipping')
lock.release()
def producer(loops):
for i in range(loops):
refill()
sleep(randrange(3))
def consumer(loops):
for i in range(loops):
buy()
sleep(randrange(3))
def _main():
print('start at:',ctime())
nloops = randrange(2,6)
print('the candt machine (full with {0} bars)!'.format(MAX))
Thread(target=consumer,args=(randrange(nloops,nloops+MAX+2),)).start()
Thread(target=producer,args=(nloops,)).start()
@register #這個不能忽略
def _atexit():
print('all done at:',ctime())
if __name__ == '__main__':
_main()
消費者生產者問題和Queue/queue模塊
. 在這個場景中,生產者生產商品,然後將其放到類似隊列的數據結構中,生產商品的時間是不確定的,同樣消費者消費商品的時間也是不確定的。
使用queue模塊(python2.x是Queue)來提供線程間通信的機制,從而讓線程之間可以互相分享數據,以下是這個模塊的常用屬性:
屬性 | 描述 |
---|---|
模塊中的類 | |
Queue(maxsize = 0) | 創建一個先入先出的隊列,如果給定參數,則在隊列沒有空間的時候堵塞,否則,爲無限隊列 |
LifoQueue(maxsize = 0) | 創建一個後入先出隊列。如果給定參數,則在隊列沒有空間時阻塞;否則,爲無限隊列 |
PriorityQueue(maxsize=0) | 創建一個優先級隊列。如果給定參數,則在隊列沒有空間時阻塞;否則,爲無限隊列 |
Queue/queue 異常 | |
Empty | 當對空隊列調用 get*()方法時拋出異常 |
Full | 當對已滿的隊列調用 put*()方法時拋出異常 |
Queue/queue 對象方法 | |
qsize () | 返回隊列大小(由於返回時隊列大小可能被其他線程修改,所以該值爲近似值) |
empty() | 如果隊列爲空,則返回 True;否則,返回 False |
full() | 如果隊列已滿,則返回 True;否則,返回 False |
put (item, block=Ture, timeout=None) | 將 item 放入隊列。如果 block 爲 True(默認)且 timeout 爲 None,則在有可用空間之前阻塞;如果timeout爲正值,則最多阻塞timeout秒;如果 block爲False,則拋出 Empty 異常 |
put_nowait(item) | 和 put(item, False)相同 |
get (block=True, timeout=None) | 從隊列中取得元素。如果給定了 block(非 0),則一直阻塞到有可用的元素爲止 |
get_nowait() | 和 get(False)相同 |
task_done() | 用於表示隊列中的某個元素已執行完成,該方法會被下面的 join()使用 |
join() | 在隊列中所有元素執行完畢並調用上面的 task_done()信號之前,保持阻塞 |
以下是相關的例子:
from random import randint
from time import sleep
from queue import Queue
import threading
class MyThread(threading.Thread):
def __init__(self,func,args,name=''):
threading.Thread.__init__(self)
self.func = func
self.args = args
self.name = name
def run(self):
self.func(*self.args)
def writeQ(queue):
print('生產出一個新商品')
queue.put('xxx',1)
print('目前商品隊列長度爲:',queue.qsize())
def readQ(queue):
print('消費了一個新商品')
val = queue.get(1)
print('當前商品隊列長度爲:',queue.qsize())
def writer(queue,loops):
for i in range(loops):
writeQ(queue)
sleep(randint(1,3))
def reader(queue,loops):
for i in range(loops):
readQ(queue)
sleep(randint(2,5))
funcs = [writer,reader]
nfuncs = range(len(funcs))
def main():
nloops = randint(2,5)
q = Queue(32)
threads = []
for i in nfuncs:
t = MyThread(funcs[i],(q,nloops),funcs[i].__name__)
threads.append(t)
for i in nfuncs:
threads[i].start()
for i in nfuncs:
threads[i].join()
print('all done')
if __name__ == '__main__':
main()
//某次執行結果如下:
生產出一個新商品
目前商品隊列長度爲: 1
消費了一個新商品
當前商品隊列長度爲: 0
生產出一個新商品
目前商品隊列長度爲: 1
生產出一個新商品
目前商品隊列長度爲: 2
消費了一個新商品
當前商品隊列長度爲: 1
消費了一個新商品
當前商品隊列長度爲: 0
all done