全局解释器锁 (Global Interpretor Lock) 说明 Python 解释器并不是线程安全的。当前线程必须持有全局锁,以便对 Python 对象进行安全地访问。因为只有一个线程可以获得 Python 对象/C API,所以解释器每经过 100 个字节码的指令,就有规律地释放和重新获得锁。解释器对线程切换进行检查的频率可以通过 sys.setcheckinterval()函数来进行控制。此外,还将根据潜在的阻塞 I/O 操作,释放和重新获得锁。有关更详细的信息,请参见参考资料部分中的 Gil and Threading State 和 Threading the Global Interpreter Lock。需要说明的是,因为 GIL,CPU 受限的应用程序将无法从线程的使用中受益。使用 Python 时,建议使用进程,或者混合创建进程和线程。 首先弄清进程和线程之间的区别,这一点是非常重要的。线程与进程的不同之处在于,它们共享状态、内存和资源。对于线程来说,这个简单的区别既是它的优势,又是它的缺点。一方面,线程是轻量级的,并且相互之间易于通信,但另一方面,它们也带来了包括死锁、争用条件和高复杂性在内的各种问题。幸运的是,由于 GIL 和队列模块,与采用其他的语言相比,采用 Python 语言在线程实现的复杂性上要低得多。
使用 Python 线程
要继续学习本文中的内容,我假定您已经安装了 Python 2.5 或者更高版本,因为本文中的许多示例都将使用 Python 语言的新特性,而这些特性仅出现于 Python2.5 之后。要开始使用 Python 语言的线程,我们将从简单的 "Hello World" 示例开始:
#! /usr/bin/env python
#coding=utf-8
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()
结果:
Thread-1 says Hello World at time: 2012-06-20 14:43:26.981173
Thread-2 says Hello World at time: 2012-06-20 14:43:26.981375仔细观察输出结果,您可以看到从两个线程都输出了 Hello World 语句,并都带有日期戳。如果分析实际的代码,那么将发现其中包含两个导入语句;一个语句导入了日期时间模块,另一个语句导入线程模块。类 ThreadClass 继承自 threading.Thread,也正因为如此,您需要定义一个 run 方法,以此执行您在该线程中要运行的代码。在这个 run 方法中唯一要注意的是,self.getName()是一个用于确定该线程名称的方法。
最后三行代码实际地调用该类,并启动线程。如果注意的话,那么会发现实际启动线程的是 t.start()。在设计线程模块时考虑到了继承,并且线程模块实际上是建立在底层线程模块的基础之上的。对于大多数情况来说,从 threading.Thread 进行继承是一种最佳实践,因为它创建了用于线程编程的常规 API。
#! /usr/bin/env python
#coding=utf-8
import urllib2
import time
import Queue
import threading
hosts = ["http://yahoo.com", "http://baidu.com", "http://amazon.com","http://ibm.com", "http://apple.com"]
queue = Queue.Queue()
class ThreadUrl(threading.Thread):
'''Theaded url grab'''
def __init__(self,queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
"""docstring for run"""
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():
"""docstring for main"""
#spawn a poll 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 中使用线程时,这个模式是一种很常见的并且推荐使用的方式。具体工作步骤描述如下:
使用多个队列
因为上面介绍的模式非常有效,所以可以通过连接附加线程池和队列来进行扩展,这是相当简单的。在上面的示例中,您仅仅输出了 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://yahoo.com", "http://baidu.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):
"""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_queuet
self.out_queue.put(chunk)
#signals to queue job is done
self.queue.task_done()
class DatamineThread(threading.Thread):
'''Thread Url Grab'''
def __init__(self, out_queue):
threading.Thread.__init__(self)
self.out_queue = out_queue
def run(self):
"""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)
分析这段代码时您可以看到,我们添加了另一个队列实例,然后将该队列传递给第一个线程池类 ThreadURL。接下来,对于另一个线程池类 DatamineThread,几乎复制了完全相同的结构。在这个类的 run 方法中,从队列中的各个线程获取 Web 页面、文本块,然后使用 Beautiful Soup 处理这个文本块。在这个示例中,使用 Beautiful Soup 提取每个页面的 title 标记、并将其打印输出。可以很容易地将这个示例推广到一些更有价值的应用场景,因为您掌握了基本搜索引擎或者数据挖掘工具的核心内容。一种思想是使用 Beautiful Soup 从每个页面中提取链接,然后按照它们进行导航。