前面我們已經通過多個線程下載csv數據並轉換爲xml文件。
在Python中由於全局解釋器鎖(GIL)的存在,多線程進行CPU密集型操作並不能提高執行效率,我們修改程序框架:
-
使用多個DownloadThread線程進行下載(I/O);
-
使用一個ConvertThread線程進行轉換(CPU);
-
下載線程把下載數據安全地傳遞給轉換線程。
要求:實現上面的程序框架。
解決方案:使用標準庫中的queue.Queue
類,它是一個線程安全的隊列。Download線程把下載數據放入隊列,Convert線程從隊列裏提取數據。
- 對於
queue.Queue
類:
class queue.Queue(maxsize=0)
FIFO隊列類。maxsize是一個整數,它設置可以放置在隊列中的項數的上限。一旦達到此大小,插入將阻塞,直到使用隊列中的項。如果maxsize小於或等於零,則隊列大小爲無窮大。
queue.Queue
類有下面方法:
Queue.qsize()
返回隊列的大致大小。注意,qsize() > 0 不保證後續的get()不被阻塞,qsize() < maxsize 也不保證put()不被阻塞。
Queue.empty()
如果隊列爲空,返回True,否則返回False。如果empty()返回True,不保證後續調用的put()不被阻塞。類似的,如果empty()返回False ,也不保證後續調用的get()不被阻塞。
Queue.full()
如果隊列是滿的返回True,否則返回False。如果full()返回True不保證後續調用的get()不被阻塞。類似的,如果full()返回False也不保證後續調用的put()不被阻塞。
Queue.put(item, block=True, timeout=None)
將 item 放入隊列。如果可選參數block是True並且timeout是None(默認),則在必要時阻塞至有空閒插槽可用。如果timeout是個正數,將最多阻塞timeout秒,如果在這段時間沒有可用的空閒插槽,將引發Full異常。反之(block是false),如果空閒插槽立即可用,則把item放入隊列,否則引發Full異常(在這種情況下,timeout將被忽略)。
Queue.put_nowait(item)
相當於put(item, False)。
Queue.get(block=True, timeout=None)
從隊列中移除並返回一個項目。如果可選參數block是True並且timeout是None(默認值),則在必要時阻塞至項目可得到。如果timeout是個正數,將最多阻塞timeout秒,如果在這段時間內項目不能得到,將引發Empty異常。反之(block是false),如果一個項目立即可得到,則返回一個項目,否則引發Empty異常(這種情況下,timeout將被忽略)。
POSIX系統3.0之前,以及所有版本的Windows系統中,如果block是True並且timeout是None,這個操作將進入基礎鎖的不間斷等待。這意味着,沒有異常能發生,尤其是SIGINT將不會觸發KeyboardInterrupt異常。
Queue.get_nowait()
相當於get(False)。
Queue.task_done()
表示前面排隊的任務已經被完成。被隊列的消費者線程使用。每個get()被用於獲取一個任務,後續調用task_done()告訴隊列,該任務的處理已經完成。
如果join()當前正在阻塞,在所有條目都被處理後,將解除阻塞(意味着每個put()進隊列的條目的task_done()都被收到)。
如果被調用的次數多於放入隊列中的項目數量,將引發ValueError異常 。
Queue.join()
阻塞至隊列中所有的元素都被接收和處理完畢。
當條目添加到隊列的時候,未完成任務的計數就會增加。每當消費者線程調用task_done()表示這個條目已經被回收,該條目所有工作已經完成,未完成計數就會減少。當未完成計數降到零的時候,join()阻塞被解除。
- 方案示例:
import requests
import base64
import csv
import time
from io import StringIO
from xml.etree.ElementTree import ElementTree, Element, SubElement
from threading import Thread
from queue import Queue
USERNAME = b'7f304a2df40829cd4f1b17d10cda0304'
PASSWORD = b'aff978c42479491f9541ace709081b99'
class DownloadThread(Thread):
def __init__(self, page_number, queue):
super().__init__()
self.page_number = page_number
self.queue = queue
def run(self):
# IO
csv_file = None
while not csv_file:
csv_file = self.download_csv(self.page_number)
self.queue.put((self.page_number, csv_file)) #存數據到隊列中
def download_csv(self, page_number):
print('download csv data [page=%s]' % page_number)
url = "https://api.intrinio.com/price.csv?ticker=AAPL&hide_paging=true&page_size=200&page_number=%s" % page_number
auth = b'Basic' + base64.b64encode(b'%s:%s' % (USERNAME, PASSWORD))
headers = {'Authorization' : auth}
response = requests.get(url, headers=headers)
if response.ok:
return StringIO(response.text)
class ConvertThread(Thread):
def __init__(self, queue):
super().__init__()
self.queue = queue
def run(self):
# CPU
while True:
page_number, csv_file = self.queue.get() #從隊列中取出數據
self.csv_to_xml(csv_file, 'data%s.xml' % page_number)
def csv_to_xml(self, csv_file, xml_path):
print('Convert csv data to %s' % xml_path)
reader = csv.reader(csv_file)
headers = next(reader)
root = Element('Data')
root.text = '\n\t'
root.tail = '\n'
for row in reader:
book = SubElement(root, 'Row')
book.text = '\n\t\t'
book.tail = '\n\t'
for tag, text in zip(headers, row):
e = SubElement(book, tag)
e.text = text
e.tail = '\n\t\t'
e.tail = '\n\t'
book.tail = '\n'
ElementTree(root).write(xml_path, encoding='utf8')
if __name__ == '__main__':
queue = Queue()
t0 = time.time()
thread_list = []
for i in range(1, 6):
t = DownloadThread(i, queue)
t.start() #啓動下載線程
thread_list.append(t)
convert_thread = ConvertThread(queue)
convert_thread.start() #啓動轉換線程
for t in thread_list:
t.join() #阻塞線程,主線程等待所有子線程結束
print(time.time() - t0)
print('main thread end.')
上面url已失效,無法看到實際耗時效果。線程間的通信可以通過queue.Queue
類創建線程安全的隊列來完成。