標籤:
為瞭解決阻塞(如I/O)問題,我們需要對程式進行並發設計。
本文將通過將線程和隊列 結合在一起,輕鬆地在 Python 中完成線程編程,建立一些簡單但有效線程使用模式。
一、使用線程
先看一個線程不多的例子,不存在阻塞,很簡單:
import threadingimport datetimeclass MyThread(threading.Thread): def run(self): now = datetime.datetime.now() print("{} says Hello World at time: {}".format(self.getName(), now)) for i in range(2): t = MyThread() t.start()
代碼解讀
1. 兩個線程都輸出了 Hello World 語句,並都帶有日期戳。
2. 兩個匯入語句:一個匯入了日期時間模組,另一個匯入線程模組。
3. 類 MyThread
繼承自 threading.Thread
,也正因為如此,您需要定義一個 run 方法,以此執行您在該線程中要啟動並執行代碼。
4. run 方法中的self.getName()
是一個用於確定該線程名稱的方法。
5. 最後三行代碼實際地調用該類,並啟動線程。如果注意的話,那麼會發現實際啟動線程的是 t.start()
。
二、使用線程隊列
如前所述,當多個線程需要共用資料或者資源的時候,可能會使得線程的使用變得複雜。線程模組提供了許多同步原語,包括訊號量、條件變數、事件和鎖。當這些 選項存在時,最佳實務是轉而關注於使用隊列。相比較而言,隊列更容易處理,並且可以使得線程編程更加安全,因為它們能夠有效地傳送單個線程對資源的所有訪 問,並支援更加清晰的、可讀性更強的設計模式。
在下一個樣本中,我們的目的是:擷取網站的 URL,並顯示頁面的前 300 個位元組。
先看看串列方式或者依次執行實現的代碼:
from urllib import requestimport timehosts = ["http://yahoo.com", "http://amazon.com", "http://ibm.com", "http://apple.com"]start = time.time()#grabs urls of hosts and prints first 1024 bytes of pagefor host in hosts: url = request.urlopen(host) print(url.read(200))print("Elapsed Time: %s" % (time.time() - start))
代碼解讀
1. urllib
模組減少了擷取 Web 頁面的複雜程度。兩次 time.time() 用於計算程式已耗用時間。
2. 這個程式的執行速度是 13.7 秒,這個結果並不算太好,也不算太糟。
3. 但如果需要檢索數百個 Web 頁面,那麼按照這個平均值,總時間需要花費大約 1000 秒的時間。如果需要檢索更多頁面呢?
下面給出線程化版本:
import queueimport threadingfrom urllib import requestimport timehosts = ["http://yahoo.com", "http://amazon.com", "http://ibm.com", "http://apple.com"]in_queue = queue.Queue()class ThreadUrl(threading.Thread): """Threaded Url Grab""" def __init__(self, in_queue): threading.Thread.__init__(self) self.in_queue = in_queue def run(self): while True: #grabs host from queue host = self.in_queue.get() #grabs urls of hosts and prints first 1024 bytes of page url = request.urlopen(host) print(url.read(200)) #signals to queue job is done self.in_queue.task_done()start = time.time()def main(): #spawn a pool of threads, and pass them queue instance for i in range(4): t = ThreadUrl(in_queue) t.setDaemon(True) t.start() #populate queue with data for host in hosts: in_queue.put(host) #wait on the queue until everything has been processed in_queue.join()main()print("Elapsed Time: %s" % (time.time() - start))
代碼解讀
1. 與第一個線程樣本相比,它並沒有複雜多少,因為使用了隊列模組。
2. 建立一個 queue.Queue()
的執行個體,然後使用資料對它進行填充。
3. 將經過填充資料的執行個體傳遞給線程類,後者是通過繼承 threading.Thread
的方式建立的。
4. 產生守護線程池。
5. 每次從隊列中取出一個項目,並使用該線程中的資料和 run 方法以執行相應的工作。
6. 在完成這項工作之後,使用 queue.task_done()
函數向任務已經完成的隊列發送一個訊號。
7. 對隊列執行 join 操作,實際上意味著等到隊列為空白,再退出主程式。
在使用這個模式時需要注意一點:通過將守護線程設定為 true,將允許主線程或者程式僅在守護線程處於活動狀態時才能夠退出。這種方式建立了一種簡單的方式以控製程序流程,因為在退出之前,您可以對隊列執行 join 操作、或者等到隊列為空白。
join()
保持阻塞狀態,直到處理了隊列中的所有項目為止。在將一個項目添加到該隊列時,未完成的任務的總數就會增加。當使用者線程調用 task_done() 以表示檢索了該項目、並完成了所有的工作時,那麼未完成的任務的總數就會減少。當未完成的任務的總數減少到零時,join()
就會結束阻塞狀態。
三、使用多個隊列
下一個樣本有兩個隊列。其中一個隊列的各線程擷取的完整 Web 頁面,然後將結果放置到第二個隊列中。然後,對加入到第二個隊列中的另一個線程池進行設定,然後對 Web 頁面執行相應的處理。
提取所訪問的每個頁面的 title 標記,並將其列印輸出。
import queueimport threadingfrom urllib import requestimport timefrom bs4 import BeautifulSouphosts = ["http://yahoo.com", "http://amazon.com", "http://ibm.com", "http://apple.com"]in_queue = queue.Queue()out_queue = queue.Queue()class ThreadUrl(threading.Thread): """Threaded Url Grab""" def __init__(self, in_queue, out_queue): threading.Thread.__init__(self) self.in_queue = in_queue self.out_queue = out_queue def run(self): while True: #grabs host from queue host = self.in_queue.get() #grabs urls of hosts and then grabs chunk of webpage url = request.urlopen(host) chunk = url.read() #place chunk into out queue self.out_queue.put(chunk) #signals to queue job is done self.in_queue.task_done()class DatamineThread(threading.Thread): """Threaded Url Grab""" def __init__(self, out_queue): threading.Thread.__init__(self) self.out_queue = out_queue def run(self): while True: #grabs host from queue chunk = self.out_queue.get() #parse the chunk soup = BeautifulSoup(chunk) print(soup.findAll([‘title‘])) #signals to queue job is done self.out_queue.task_done()start = time.time()def main(): #spawn a pool of threads, and pass them queue instance for i in range(4): t = ThreadUrl(in_queue, out_queue) t.setDaemon(True) t.start() #populate queue with data for host in hosts: in_queue.put(host) for i in range(4): dt = DatamineThread(out_queue) dt.setDaemon(True) dt.start() #wait on the queue until everything has been processed in_queue.join() out_queue.join()main()print("Elapsed Time: %s" % (time.time() - start))
代碼解讀
1. 我們添加了另一個隊列執行個體,然後將該隊列傳遞給第一個線程池類 ThreadURL
。
2. 對於另一個線程池類 DatamineThread
, 幾乎複製了完全相同的結構。
3. 在這個類的 run 方法中,從隊列中的各個線程擷取 Web 頁面、文字區塊,然後使用 Beautiful Soup 處理這個文字區塊。
4. 使用 Beautiful Soup 提取每個頁面的 title 標記、並將其列印輸出。
5. 可以很容易地將這個樣本推廣到一些更有價值的應用情境,因為您掌握了基本搜尋引擎或者資料採礦工具的核心內容。
6. 一種思想是使用 Beautiful Soup 從每個頁面中提取連結,然後按照它們進行導航。
python線程的使用模式