使用 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”论坛:
相关文章
相关标签/搜索