39. 線程間通信

前面我們已經通過多個線程下載csv數據並轉換爲xml文件。

在Python中由於全局解釋器鎖(GIL)的存在,多線程進行CPU密集型操作並不能提高執行效率,我們修改程序框架:

  1. 使用多個DownloadThread線程進行下載(I/O);

  2. 使用一個ConvertThread線程進行轉換(CPU);

  3. 下載線程把下載數據安全地傳遞給轉換線程。

要求:實現上面的程序框架。

解決方案:使用標準庫中的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類創建線程安全的隊列來完成。


發佈了225 篇原創文章 · 獲贊 227 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章