对于Python来说,并不缺少并发选项,其标准库包括了对线程、进程和异步I/O的支持。在许多情况下,通过创建诸如异步、线程和子进程之类的高层模块,Python简化了各种并发方法的使用。除了标准库之外,还有一些第三方的解决方案。例如Twisted、Stackless和进程Module。因为GIL,CPU受限的应用程序无法从线程中受益。使用Python时,建议使用进程,或者混合创建进程和线程。
首先弄清楚进程和线程的区别。线程和进程的不同之处在于,它们共享状态、内存和资源。对于线程来说,这个简单的区别既是它的优势,又是它的缺点。一方面,线程是轻量级的,并且相互之间易于通信,但另一方面,它们也带来了包括死锁、争用条件和好复杂性在内的各种问题。幸运的是,由于GIL和队列模块,与采用其他的语言相比,采用Python语言在线程实现的复杂性上要低的多。
一个简单的demo:
#!/usr/bin/env python import Queue import threading import urllib2 import time hosts = ["http://www.baidu.com", "http://www.sina.com.cn", "http://www.letv.com"] class ThreadUrl(threading.Thread): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: host = self.queue.get() url = urllib2.urlopen(host) print url.read(1024) self.queue.task_done() def main(): for i in range(5): t = ThreadUrl(queue) t.setDaemon(True) t.start() for host in hosts: queue.put(host) queue.join() if __name__ == "__main__": start = time.time() main() print "Elapsed Time:%s" % (time.time() - start)
在Python中使用线程时,这个模型是一种很常见的并且推荐使用的方式。具体工作步骤描述如下:
1. 创建一个Queue.Queue()队列实例,然后向这个队列内灌数据。
2. 将灌进数据的实例传递给线程类,然后通过继承threading.Thread的方式创建。
3. 生成守护进程池(t.setDaemon(True))。
4. 每次从queue中pop一个项目,并使用该线程中的数据和run方法以执行相应的工作。
5. 在完成这项工作之后,使用queue.task_done()方法向任务完成的队列发送一个信号。
6. 对队列执行join操作,实际上意味着等到queue为空,再退出主程序。
在使用这个模式时需要注意一点:通过将守护线程设置为True,将允许主线程或者程序仅在守护线程处于活动状态时才能够退出。这种方式创建了一种简单的方式以控制程序流程,因为在退出之前,你可以对queue执行join操作或者等到队列为空。
说明:
join():保持阻塞状态,直到处理了队列中的所有项目为止。在将一个项目添加到该队列时,未完成的任务的总数就会增加。当使用者线程调用task_done()以表示检索了该项目、并完成了所有的工作时,那么未完成的任务的总数就会减少。当未完成的任务的总数减少到零时,join()就会结束阻塞状态。
使用多队列:
因为上面介绍的模式非常有效,所以可以通过连接附加线程池和队列来进行扩展,这是相当简单的。在上面的示例中,您仅仅输出了 Web 页面的开始部分。而下一个示例则将返回各线程获取的完整 Web 页面,然后将结果放置到另一个队列中。然后,对加入到第二个队列中的另一个线程池进行设置,然后对 Web 页面执行相应的处理。这个示例中所进行的工作包括使用一个名为 Beautiful Soup 的第三方 Python 模块来解析 Web 页面。使用这个模块,您只需要两行代码就可以提取所访问的每个页面的 title 标记,并将其打印输出。
代码片段:
#!/usr/bin/env python #-*- coding:utf-8 -*- import Queue import threading import urllib2 import time from BeautifulSoup import BeautifulSoup hosts = ['http://www.baidu.com', 'http://www.letv.com', 'http://www.sina.com.cn', 'http://www.sohu.com', 'http://www.jd.com', 'http://WWW.51CTO.COM', 'http://www.baidu.com', 'http://www.sina.com.cn', 'http://www.sohu.com'] queue = Queue.Queue() out_queue = Queue.Queue() class ThreadUrl(threading.Thread): def __init__(self, queue, out_queue): threading.Thread.__init__(self) self.queue = queue self.out_queue = out_queue def run(self): while True: host = self.queue.get() url = urllib2.urlopen(host) chunk = url.read() self.out_queue.put(chunk) self.queue.task_done() class DatamineThread(threading.Thread): def __init__(self, out_queue): threading.Thread.__init__(self) self.out_queue = out_queue def run(self): while True: try: chunk = self.out_queue.get() soup = BeautifulSoup(chunk) print soup.findAll(['title']) self.out_queue.task_done() except: pass def main(): for i in range(5): t = ThreadUrl(queue, out_queue) t.setDaemon(True) t.start() for host in hosts: queue.put(host) for i in range(5): dt = DatamineThread(out_queue) dt.setDaemon(True) dt.start() queue.join() out_queue.join() if __name__ == "__main__": start = time.time() main() print "Elapsed Time: %s" % (time.time() - start)
执行结果:
通过该代码您可以看到,我们添加了另一个队列的实例,然后将该队列传递给第一个线程池类ThreadUrl.接下来,对于另一个线程池DatamineThread,几乎复制了完全相同的结构。在这个类的run方法中,从队列中的各个线程获取web页面、文本块,然后使用Beautiful Soup处理这个文本块。在这个实例中,使用Beautiful Soup提取每个页面的title标记、并将其打印输出。可以很容易地将这个实例推广到一些更有价值的应用场景,因为您掌握了基本搜索引擎或者数据挖掘工具的核心内容。一种思想是使用Beautiful Soup从每个页面提取链接,然后按照它们进行导航。
总结:
本文研究了 Python 的线程,并且说明了如何使用队列来降低复杂性和减少细微的错误、并提高代码可读性的最佳实践。尽管这个基本模式比较简单,但可以通过将队列和线程池连接在一起,以便将这个模式用于解决各种各样的问题。
最后,还有很重要的一点需要指出,线程并不能解决所有的问题,对于许多情况,使用进程可能更为合适。特别是,当您仅需要创建许多子进程并对响应进行侦听时,那么标准库子进程模块可能使用起来更加容易。