??????曾經(jīng)有這么一個(gè)說法,程序中存在3種類型的bug:你的bug,我的bug和多線程。這雖然是句調(diào)侃,但從某種程度上道出了一個(gè)事實(shí):多線程編程不是件容易的事情。線程間的同步和互斥,線程間數(shù)據(jù)的共享等這些都是涉及線程安全方面要考慮的問題。縱然python中提供了眾多的同步和互斥機(jī)制,如mutex,condition,event等,但同步和互斥本身就不是一個(gè)容易的話題,稍有不慎就會(huì)陷入死鎖狀態(tài)或者威脅線程安全。我們來看一個(gè)經(jīng)典的多線程同步問題:生產(chǎn)者消費(fèi)者模型。如果用python來實(shí)現(xiàn),你會(huì)怎么寫?大概思路是這樣:分別創(chuàng)建消費(fèi)者和生產(chǎn)者線程,生產(chǎn)者往隊(duì)列里面放產(chǎn)品,消費(fèi)者從隊(duì)列里面取出產(chǎn)品,創(chuàng)建一個(gè)線程鎖以保證線程間操作的互斥性。當(dāng)隊(duì)列滿足的時(shí)候消費(fèi)者進(jìn)入等待狀態(tài),當(dāng)隊(duì)列為空的時(shí)候生產(chǎn)者進(jìn)入等待狀態(tài)。我們來看一個(gè)具體的python實(shí)現(xiàn):
# -*- coding: utf-8 -*-
import Queue
import threading
import random
writelock = threading.Lock()
class Producer(threading.Thread):
def __init__(self, q, con, name):
super(Producer, self).__init__()
self.q = q
self.name = name
self.con = con
print "Producer " + self.name + " Started"
def run(self):
while 1:
global writelock
self.con.acquire() # 獲取鎖對(duì)象
if self.q.full():
with writelock:
print 'Queue is full, producer wait!'
self.con.wait() # 等待資源
else:
value = random.randint(0, 10)
with writelock:
print self.name + " put value" + self.name + ":" + str(value)+ "into queue"
self.q.put((self.name + ":" + str(value)))
self.con.notify()
self.con.release()
class Consumer(threading.Thread):
def __init__(self, q, con, name):
super(Consumer, self).__init__()
self.q = q
self.con = con
self.name = name
print "Consumer "+self.name+" started\n"
def run(self):
while 1:
global writelock
self.con.acquire()
if self.q.empty():
with writelock:
print 'queue is empty, consumer wait!'
self.con.wait()
else:
value = self.q.get()
with writelock:
print self.name + "get value" + value + " from queue"
self.con.notify()
self.con.release()
if __name__ == "__main__":
q = Queue.Queue(10)
con = threading.Condition()
p = Producer(q, con, "P1")
p.start()
p1 = Producer(q, con, "P2")
p1.start()
c1 = Consumer(q, con, "C1")
c1.start()
??????上面的程序?qū)崿F(xiàn)有什么問題嗎?回答這個(gè)問題之前,我們先來了解一下Queue模塊的基本知識(shí)。Python中的Queue模塊提供了3種隊(duì)列:
- Queue.Queue(maxsize): 先進(jìn)先出,maxsize為隊(duì)列大小,其值為非正數(shù)非時(shí)候?yàn)闊o限循環(huán)隊(duì)列
- Queue.LifoQueue(maxsize): 后進(jìn)先出,相當(dāng)于棧
- Queue.PriorityQueue(maxsize): 優(yōu)先級(jí)隊(duì)列。
這三種隊(duì)列支持以下方法: - Queue.qsize(): 返回近似的隊(duì)列大小。注意,這里之所以加“近似”二字,是因?yàn)樵撝?gt;0的時(shí)候并不保證并發(fā)執(zhí)行的時(shí)候get()方法不被阻塞,同樣,對(duì)于put()方法有效
- Queue.empty(): 隊(duì)列為空的時(shí)候返回True, 否則返回False
- Queue.full(): 當(dāng)設(shè)定了隊(duì)列大小的時(shí)候,如果隊(duì)列滿則返回True,否則返回False
- Queue.put(item[,block[, timeout]]): 往隊(duì)列里添加元素item,block設(shè)置為False的時(shí)候,如果對(duì)列滿則拋出Full異常。如果block設(shè)置為True,timeout為None的時(shí)候則會(huì)一直等待到有空位置,否則會(huì)根據(jù)timeout的設(shè)定超時(shí)后拋出full異常
- Queue.put_nowait(item): 等價(jià)于put(item, False).block設(shè)置為False的時(shí)候,如果對(duì)列空咋拋出Empty異常。如果block設(shè)置為True,timeout為None的時(shí)候則會(huì)一直等待到有元素可用,否則會(huì)根據(jù)timeout的設(shè)定超時(shí)后拋出empty異常
- Queue.get([block[,timeout]]): 從隊(duì)列中刪除元素并返回該元素的值
- Queue.get_nowait(): 等價(jià)于get(False)
- Queue.task_done(): 發(fā)送信號(hào)表明入隊(duì)任務(wù)已經(jīng)完成,經(jīng)常在消費(fèi)者線程中用到
- Queue.join(): 阻塞直至對(duì)列中所有元素處理完畢
??????Queue模塊實(shí)現(xiàn)了多個(gè)生產(chǎn)者消費(fèi)者的對(duì)列,當(dāng)線程之間需要信息安全的交換的時(shí)候特別的有用,因此這個(gè)模塊實(shí)現(xiàn)了所需要的鎖原語,為Python多線程編程提供了有力的支持,踏實(shí)線程安全的。需要注意的是Queue模塊中的對(duì)列和collections.dequeue所表示對(duì)額對(duì)列并不一樣,前者主要用于不同線程之間的通信,它內(nèi)部實(shí)現(xiàn)了現(xiàn)成的鎖機(jī)制;而后者主要是數(shù)據(jù)結(jié)構(gòu)上的概念,因此支持in方法
??????再回過頭來看看前面的例子,程序的實(shí)現(xiàn)有什么問題呢?答案很明顯,作用于queue操作的條件變量完全是不需要的,因?yàn)镼ueue本身能夠保證線程安全,因此不需要額外的同步機(jī)制。下面是一個(gè)多線程下載的例子
# -*- coding: utf-8 -*-
import os
import Queue
import threading
import urllib2
class DownloadThread(threading.Thread):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
url = self.queue.get()
print self.name + "begin download"+url +"..."
self.download_file(url)
self.queue.task_done()
print self.name + " download completed!!!"
def download_file(self, url):
urlhandler = urllib2.urlopen(url)
fname = os.path.basename(url) + ".html"
with open(fname, 'wb') as f:
while True:
chunk = urlhandler.read(1024)
if not chunk:
break
f.write(chunk)
if __name__ == "__main__":
urls = [ "http://www.baidu.com", "http://www.cnblogs.com/chaosimple/p/4153083.html",
"http://shop.dspread.com/weixin/ksher/check_shop?page_no=1&page_count=10"]
queue = Queue.Queue()
for i in range(5):
t = DownloadThread(queue)
t.setDaemon(True)
t.start()
for url in urls:
queue.put(url)
queue.join()