使用 Python 進行線程編程

對於 Python 來講,並不缺乏併發選項,其標準庫中包括了對線程、進程和異步 I/O 的支持。在許多狀況下,經過建立諸如異步、線程和子進程之類的高層模塊,Python 簡化了各類併發方法的使用。除了標準庫以外,還有一些第三方的解決方案,例如 Twisted、Stackless 和進程模塊。本文重點關注於使用 Python 的線程,並使用了一些實際的示例進行說明。雖然有許多很好的聯機資源詳細說明了線程 API,但本文嘗試提供一些實際的示例,以說明一些常見的線程使用模式。 html

全局解釋器鎖 (Global Interpretor Lock) 說明 Python 解釋器並非線程安全的。當前線程必須持有全局鎖,以便對 Python 對象進行安全地訪問。由於只有一個線程能夠得到 Python 對象/C API,因此解釋器每通過 100 個字節碼的指令,就有規律地釋放和從新得到鎖。解釋器對線程切換進行檢查的頻率能夠經過sys.setcheckinterval()函數來進行控制。 python

此外,還將根據潛在的阻塞 I/O 操做,釋放和從新得到鎖。有關更詳細的信息,請參見參考資料部分中的 Gil and Threading StateThreading the Global Interpreter Locklinux

須要說明的是,由於 GIL,CPU 受限的應用程序將沒法從線程的使用中受益。使用 Python 時,建議使用進程,或者混合建立進程和線程。 web

首先弄清進程和線程之間的區別,這一點是很是重要的。線程與進程的不一樣之處在於,它們共享狀態、內存和資源。對於線程來 說,這個簡單的區別既是它的優點,又是它的缺點。一方面,線程是輕量級的,而且相互之間易於通訊,但另外一方面,它們也帶來了包括死鎖、爭用條件和高複雜性 在內的各類問題。幸運的是,因爲 GIL 和隊列模塊,與採用其餘的語言相比,採用 Python 語言在線程實現的複雜性上要低得多。 編程

使用 Python 線程 設計模式

要繼續學習本文中的內容,我假定您已經安裝了 Python 2.5 或者更高版本,由於本文中的許多示例都將使用 Python 語言的新特性,而這些特性僅出現於 Python2.5 以後。要開始使用 Python 語言的線程,咱們將從簡單的 "Hello World" 示例開始: api


hello_threads_example
import threading
        import datetime
        
        class ThreadClass(threading.Thread):
          def run(self):
            now = datetime.datetime.now()
            print "%s says Hello World at time: %s" % 
            (self.getName(), now)
        
        for i in range(2):
          t = ThreadClass()
          t.start()

若是運行這個示例,您將獲得下面的輸出: 安全

# python hello_threads.py 
      Thread-1 says Hello World at time: 2008-05-13 13:22:50.252069
      Thread-2 says Hello World at time: 2008-05-13 13:22:50.252576

仔細觀察輸出結果,您能夠看到從兩個線程都輸出了 Hello World 語句,並都帶有日期戳。若是分析實際的代碼,那麼將發現其中包含兩個導入語句;一個語句導入了日期時間模塊,另外一個語句導入線程模塊。類ThreadClass繼承自threading.Thread,也正由於如此,您須要定義一個 run 方法,以此執行您在該線程中要運行的代碼。在這個 run 方法中惟一要注意的是,self.getName()是一個用於肯定該線程名稱的方法。 服務器

最後三行代碼實際地調用該類,並啓動線程。若是注意的話,那麼會發現實際啓動線程的是t.start()。在設計線程模塊時考慮到了繼承,而且線程模塊其實是創建在底層線程模塊的基礎之上的。對於大多數狀況來講,從threading.Thread進行繼承是一種最佳實踐,由於它建立了用於線程編程的常規 API。 網絡

使用線程隊列

如前所述,當多個線程須要共享數據或者資源的時候,可能會使得線程的使用變得複雜。線程模塊提供了許多同步原語,包括信號 量、條件變量、事件和鎖。當這些選項存在時,最佳實踐是轉而關注於使用隊列。相比較而言,隊列更容易處理,而且可使得線程編程更加安全,由於它們可以有 效地傳送單個線程對資源的全部訪問,並支持更加清晰的、可讀性更強的設計模式。

在下一個示例中,您將首先建立一個以串行方式或者依次執行的程序,獲取網站的 URL,並顯示頁面的前 1024 個字節。有時使用線程能夠更快地完成任務,下面就是一個典型的示例。首先,讓咱們使用urllib2模塊以獲取這些頁面(一次獲取一個頁面),而且對代碼的運行時間進行計時:


URL 獲取序列
import urllib2
        import time
        
        hosts = ["http://yahoo.com", "http://google.com", "http://amazon.com",
        "http://ibm.com", "http://apple.com"]
        
        start = time.time()
        #grabs urls of hosts and prints first 1024 bytes of page
        for host in hosts:
          url = urllib2.urlopen(host)
          print url.read(1024)
        
        print "Elapsed Time: %s" % (time.time() - start)

在運行以上示例時,您將在標準輸出中得到大量的輸出結果。但最後您將獲得如下內容:

Elapsed Time: 2.40353488922

讓咱們仔細分析這段代碼。您僅導入了兩個模塊。首先,urllib2模塊減小了工做的複雜程度,而且獲取了 Web 頁面。而後,經過調用time.time(), 您建立了一個開始時間值,而後再次調用該函數,而且減去開始值以肯定執行該程序花費了多長時間。最後分析一下該程序的執行速度,雖然「2.5 秒」這個結果並不算太糟,但若是您須要檢索數百個 Web 頁面,那麼按照這個平均值,就須要花費大約 50 秒的時間。研究如何建立一種能夠提升執行速度的線程化版本:


URL 獲取線程化
#!/usr/bin/env python
          import Queue
          import threading
          import urllib2
          import time
          
          hosts = ["http://yahoo.com", "http://google.com", "http://amazon.com",
          "http://ibm.com", "http://apple.com"]
          
          queue = Queue.Queue()
          
          class ThreadUrl(threading.Thread):
          """Threaded Url Grab"""
            def __init__(self, queue):
              threading.Thread.__init__(self)
              self.queue = queue
          
            def run(self):
              while True:
                #grabs host from queue
                host = self.queue.get()
            
                #grabs urls of hosts and prints first 1024 bytes of page
                url = urllib2.urlopen(host)
                print url.read(1024)
            
                #signals to queue job is done
                self.queue.task_done()
          
          start = time.time()
          def main():
          
            #spawn a pool of threads, and pass them queue instance 
            for i in range(5):
              t = ThreadUrl(queue)
              t.setDaemon(True)
              t.start()
              
           #populate queue with data   
              for host in hosts:
                queue.put(host)
           
           #wait on the queue until everything has been processed     
           queue.join()
          
          main()
          print "Elapsed Time: %s" % (time.time() - start)

對於這個示例,有更多的代碼須要說明,但與第一個線程示例相比,它並無複雜多少,這正是由於使用了隊列模塊。在 Python 中使用線程時,這個模式是一種很常見的而且推薦使用的方式。具體工做步驟描述以下:

  1. 建立一個Queue.Queue()的實例,而後使用數據對它進行填充。
  2. 將通過填充數據的實例傳遞給線程類,後者是經過繼承threading.Thread的方式建立的。
  3. 生成守護線程池。
  4. 每次從隊列中取出一個項目,並使用該線程中的數據和 run 方法以執行相應的工做。
  5. 在完成這項工做以後,使用queue.task_done()函數向任務已經完成的隊列發送一個信號。
  6. 對隊列執行 join 操做,實際上意味着等到隊列爲空,再退出主程序。

在使用這個模式時須要注意一點:經過將守護線程設置爲 true,將容許主線程或者程序僅在守護線程處於活動狀態時纔可以退出。這種方式建立了一種簡單的方式以控制程序流程,由於在退出以前,您能夠對隊列執行 join 操做、或者等到隊列爲空。隊列模塊文檔詳細說明了實際的處理過程,請參見參考資料

join()
保持阻塞狀態,直處處理了隊列中的全部項目爲止。在將一個項目添加到該隊列時,未完成的任務的總數就會增長。當使用者線程調用 task_done() 以表示檢索了該項目、並完成了全部的工做時,那麼未完成的任務的總數就會減小。當未完成的任務的總數減小到零時,join()就會結束阻塞狀態。

回頁首

使用多個隊列

由於上面介紹的模式很是有效,因此能夠經過鏈接附加線程池和隊列來進行擴展,這是至關簡單的。在上面的示例中,您僅僅輸出了 Web 頁面的開始部分。而下一個示例則將返回各線程獲取的完整 Web 頁面,而後將結果放置到另外一個隊列中。而後,對加入到第二個隊列中的另外一個線程池進行設置,而後對 Web 頁面執行相應的處理。這個示例中所進行的工做包括使用一個名爲 Beautiful Soup 的第三方 Python 模塊來解析 Web 頁面。使用這個模塊,您只須要兩行代碼就能夠提取所訪問的每一個頁面的 title 標記,並將其打印輸出。


多隊列數據挖掘網站
import Queue
import threading
import urllib2
import time
from BeautifulSoup import BeautifulSoup

hosts = ["http://yahoo.com", "http://google.com", "http://amazon.com",
        "http://ibm.com", "http://apple.com"]

queue = Queue.Queue()
out_queue = Queue.Queue()

class ThreadUrl(threading.Thread):
    """Threaded Url Grab"""
    def __init__(self, queue, out_queue):
        threading.Thread.__init__(self)
        self.queue = queue
        self.out_queue = out_queue

    def run(self):
        while True:
            #grabs host from queue
            host = self.queue.get()

            #grabs urls of hosts and then grabs chunk of webpage
            url = urllib2.urlopen(host)
            chunk = url.read()

            #place chunk into out queue
            self.out_queue.put(chunk)

            #signals to queue job is done
            self.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(5):
        t = ThreadUrl(queue, out_queue)
        t.setDaemon(True)
        t.start()

    #populate queue with data
    for host in hosts:
        queue.put(host)

    for i in range(5):
        dt = DatamineThread(out_queue)
        dt.setDaemon(True)
        dt.start()


    #wait on the queue until everything has been processed
    queue.join()
    out_queue.join()

main()
print "Elapsed Time: %s" % (time.time() - start)

若是運行腳本的這個版本,您將獲得下面的輸出:

# python url_fetch_threaded_part2.py 

  [<title>Google</title>]
  [<title>Yahoo!</title>]
  [<title>Apple</title>]
  [<title>IBM United States</title>]
  [<title>Amazon.com: Online Shopping for Electronics, Apparel,
 Computers, Books, DVDs & more</title>]
  Elapsed Time: 3.75387597084

分析這段代碼時您能夠看到,咱們添加了另外一個隊列實例,而後將該隊列傳遞給第一個線程池類ThreadURL。接下來,對於另外一個線程池類DatamineThread, 幾乎複製了徹底相同的結構。在這個類的 run 方法中,從隊列中的各個線程獲取 Web 頁面、文本塊,而後使用 Beautiful Soup 處理這個文本塊。在這個示例中,使用 Beautiful Soup 提取每一個頁面的 title 標記、並將其打印輸出。能夠很容易地將這個示例推廣到一些更有價值的應用場景,由於您掌握了基本搜索引擎或者數據挖掘工具的核心內容。一種思想是使用 Beautiful Soup 從每一個頁面中提取連接,而後按照它們進行導航。

回頁首

總結

本文研究了 Python 的線程,而且說明了如何使用隊列來下降複雜性和減小細微的錯誤、並提升代碼可讀性的最佳實踐。儘管這個基本模式比較簡單,但能夠經過將隊列和線程池鏈接在 一塊兒,以便將這個模式用於解決各類各樣的問題。在最後的部分中,您開始研究如何建立更復雜的處理管道,它能夠用做將來項目的模型。參考資料部分提供了不少有關常規併發性和線程的極好的參考資料。

最後,還有很重要的一點須要指出,線程並不能解決全部的問題,對於許多狀況,使用進程可能更爲合適。特別是,當您僅須要建立許多子進程並對響應進行偵聽時,那麼標準庫子進程模塊可能使用起來更加容易。有關更多的官方說明文檔,請參考參考資料部分。


回頁首

下載

描述 名字 大小 下載方法
Sample threading code for this article threading_code.zip 24KB HTTP

關於下載方法的信息


參考資料

學習

  • 您能夠參閱本文在 developerWorks 全球站點上的 英文原文

  • 這個線程模塊爲多線程的使用提供了底層原語。

  • 這個線程化模塊在較低層次線程模塊的基礎上構造了高層次的線程接口。

  • PMOTW 線程模塊容許您在相同的進程空間中併發地執行多項操做。

  • GIL 和線程狀態

  • 閱讀 Threading the Global Interpreter Lock

  • 併發和 Python

  • Asyncore 模塊提供了以異步的方式寫入套接字服務客戶端和服務器的基礎結構。

  • 瞭解 Wikipedia 如何定義線程

  • 瞭解如何在軟件中實現併發 Free Lunch Is Over

  • 隊列模塊

  • Beautiful Soup 是一種面向 Python 語言的 HTML/XML 解析器,它甚至能夠將無效的標記轉換爲解析樹。

  • 子線程模塊容許您生成新的進程,鏈接到它們的輸入/輸出/錯誤管道,並獲取它們的返回代碼。

  • AIX and UNIX 專區:developerWorks 的「AIX and UNIX 專區」提供了大量與 AIX 系統管理的全部方面相關的信息,您能夠利用它們來擴展本身的 UNIX 技能。

  • AIX and UNIX 新手入門:訪問「AIX and UNIX 新手入門」頁面可瞭解更多關於 AIX 和 UNIX 的內容。

  • AIX and UNIX 專題彙總:AIX and UNIX 專區已經爲您推出了不少的技術專題,爲您總結了不少熱門的知識點。咱們在後面還會繼續推出不少相關的熱門專題給您,爲了方便您的訪問,咱們在這裏爲您把本專區的全部專題進行彙總,讓您更方便的找到您須要的內容。

  • developerWorks 技術事件和網絡廣播:瞭解最新的 developerWorks 技術事件和網絡廣播。

  • Podcast:收聽 Podcast 並與 IBM 技術專家保持同步。

得到產品和技術

  • IBM 試用軟件:從 developerWorks 可直接下載這些試用軟件,您能夠利用它們開發您的下一個項目。

討論

參與「AIX and UNIX」論壇:
相關文章
相關標籤/搜索