40. 在線程間進行事件通知

在之前通過使用多個DownloadThread線程進行下載(I/O)及使用一個ConvertThread線程進行轉換(CPU),我們達到了多線程下載csv數據並轉換爲xml文件的目的。但現在有額外的要求:

實現一個打包線程TarThread,將轉換出的xml文件壓縮打包。例如轉換線程每生產出5個xml文件,就通知打包線程將它們打包成一個xxx.tgz文件,並刪除xml文件。打包完成後,打包線程反過來通知轉換進程,轉換進程繼續轉換。

解決方案:

線程間的事件通知,可以使用標準庫中Threading.Event類:

  1. 等待事件,一端調用wait()方法;

  2. 通知事件,一端調用set()方法。


  • 對於threading.Event類:
class threading.Event

實現事件對象的類。事件對象管理一個內部標誌,調用set()方法可將其設置爲true。調用clear()方法可將其設置爲false。調用wait()方法將進入阻塞直到標誌爲true。這個標誌初始時爲false。

threading.Event類有以下方法:

is_set()

當且僅當內部標誌爲True時返回True。

set()

將內部標誌設置爲True。所有正在等待這個事件的線程將被喚醒。當標誌爲True時,調用wait()方法的線程不會被被阻塞。

clear()

將內部標誌設置爲Talse。之後調用wait()方法的線程將會被阻塞,直到調用set()方法將內部標誌再次設置爲true。

wait(timeout=None)

阻塞線程直到內部變量爲True。如果調用時內部標誌爲True,將立即返回。否則將阻塞線程,直到調用set()方法將標誌設置爲True或者發生可選的超時。

當提供了timeout參數且不是None時,它應該是一個浮點數,代表操作的超時時間,以秒爲單位(可以爲小數)。

當內部標誌在調用wait進入阻塞後被設置爲True,或者調用wait時已經被設置爲True時,方法返回True。也就是說,除非設定了超時且發生了超時的情況下將會返回False,其他情況該方法都將返回True。

  • 方案示例:
import requests
import base64
import csv
import time
import os
import tarfile
from io import StringIO
from xml.etree.ElementTree import ElementTree, Element, SubElement
from threading import Thread,Event
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, c_event, t_event):
        super().__init__()
        self.queue = queue
        self.c_event = c_event
        self.t_event = t_event

    def run(self):
        count = 0
        while True:
            page_number, csv_file = self.queue.get()                #從隊列中取出數據
            if page_number == -1:
                self.c_event.set()
                self.c_event.wait()             #當最後不足5個時也打包
                break
            count += 1
            self.csv_to_xml(csv_file, 'data%s.xml' % page_number)

            if count == 5:
                # 通知轉換完成
                self.c_event.set()

                #等待打包完成
                self.c_event.wait()
                self.c_event.clear()
                count = 0

    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')


class TarThread(Thread):
    def __init__(self, c_event, t_event):
        super().__init__(daemon=True)               #守護線程
        self.count = 0
        self.c_event = c_event
        self.t_event = t_event

    def run(self):
        while True:
            # 等待轉換完成
            self.c_event.wait()
            self.c_event.clear()

            # 打包
            self.tar_xml()

            # 通知打包完成
            self.c_event.set()

    def tar_xml(self):
        self.count += 1
        tfname = 'data%s.tgz' % self.count
        print('tar %s...' % tfname)
        tf = tarfile.open(tfname, 'w:gz')               #tar壓縮
        for fname in os.listdir('.'):
            if fname.endswith('.xml'):
                tf.add(fname)
                os.remove(fname)
        tf.close()

        if not tf.members:
            os.remove(tfname)


if __name__ == '__main__':
    queue = Queue()
    c_event = Event()
    t_event = Event()
    t0 = time.time()
    thread_list = []
    for i in range(1, 11):
        t = DownloadThread(i, queue)
        t.start()               #啓動下載線程
        thread_list.append(t)

    convert_thread = ConvertThread(queue, c_event, t_event)
    convert_thread.start()              #啓動轉換線程

    tar_thread = TarThread(c_event, t_event)
    tar_thread.start()              #啓動打包線程

    for t in thread_list:
        t.join()                #阻塞線程,主線程等待所有子線程結束

    # 通知Convert線程退出
    queue.put((-1, None))               #將page_number置爲-1

    # 等待轉換線程結束
    convert_thread.join()

    print(time.time() - t0)
    print('main thread end.')

在線程間進行事件通知,目的就是線程間同步。


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