使用Python中的线程进行网络编程的入门教程_python

引言

对于 Python 来说,并不缺少并发选项,其标准库中包括了对线程、进程和异步 I/O 的支持。在许多情况下,通过创建诸如异步、线程和子进程之类的高层模块,Python 简化了各种并发方法的使用。除了标准库之外,还有一些第三方的解决方案,例如 Twisted、Stackless 和进程模块。本文重点关注于使用 Python 的线程,并使用了一些实际的示例进行说明。虽然有许多很好的联机资源详细说明了线程 API,但本文尝试提供一些实际的示例,以说明一些常见的线程使用模式。

全局解释器锁 (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" 示例开始:
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 中使用线程时,这个模式是一种很常见的并且推荐使用的方式。具体工作步骤描述如下:

  •     创建一个 Queue.Queue() 的实例,然后使用数据对它进行填充。
  •     将经过填充数据的实例传递给线程类,后者是通过继承 threading.Thread 的方式创建的。
  •     生成守护线程池。
  •     每次从队列中取出一个项目,并使用该线程中的数据和 run 方法以执行相应的工作。
  •     在完成这项工作之后,使用 queue.task_done() 函数向任务已经完成的队列发送一个信号。
  •     对队列执行 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 的线程,并且说明了如何使用队列来降低复杂性和减少细微的错误、并提高代码可读性的最佳实践。尽管这个基本模式比较简单,但可以通过将队列和线程池连接在一起,以便将这个模式用于解决各种各样的问题。在最后的部分中,您开始研究如何创建更复杂的处理管道,它可以用作未来项目的模型。参考资料部分提供了很多有关常规并发性和线程的极好的参考资料。

最后,还有很重要的一点需要指出,线程并不能解决所有的问题,对于许多情况,使用进程可能更为合适。特别是,当您仅需要创建许多子进程并对响应进行侦听时,那么标准库子进程模块可能使用起来更加容易。有关更多的官方说明文档,请参考参考资料部分。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索python
python编程入门教程、c语言多线程编程入门、python编程入门、python多线程编程、python编程入门经典,以便于您获取更多的相关知识。

时间: 2024-10-18 13:39:09

使用Python中的线程进行网络编程的入门教程_python的相关文章

Python中一些自然语言工具的使用的入门教程_python

NLTK 是使用 Python 教学以及实践计算语言学的极好工具.此外,计算语言学与人工 智能.语言/专门语言识别.翻译以及语法检查等领域关系密切.NLTK 包括什么 NLTK 会被自然地看作是具有栈结构的一系列层,这些层构建于彼此基础之上.那些熟悉人工语言(比如 Python)的文法 和解析的读者来说,理解自然语言模型中类似的 -- 但更深奥的 -- 层不会有太大困难. 术语表 全集(Corpora):相关文本的集合.例如,莎士比亚的作品可能被统称为一个 文集(corpus): 而若干个作者的

Python的requests网络编程包使用教程_python

早就听说requests的库的强大,只是还没有接触,今天接触了一下,发现以前使用urllib,urllib2等方法真是太搓了-- 这里写些简单的使用初步作为一个记录 一.下载 官方项目页: https://pypi.python.org/pypi/requests/#downloads 可以从上面直接下载. 二.发送无参数的get请求 >>> r = requests.get('http://httpbin.org/get') >>> print r.text { &q

使用Python中的greenlet包实现并发编程的入门教程_python

1   动机 greenlet 包是 Stackless 的副产品,其将微线程称为 "tasklet" .tasklet运行在伪并发中,使用channel进行同步数据交换. 一个"greenlet",是一个更加原始的微线程的概念,但是没有调度,或者叫做协程.这在你需要控制你的代码时很有用.你可以自己构造微线程的 调度器:也可以使用"greenlet"实现高级的控制流.例如可以重新创建构造器:不同于Python的构造器,我们的构造器可以嵌套的调用函

Python中使用pprint函数进行格式化输出的教程_python

pprint – 美观打印 作用:美观打印数据结构 pprint 包含一个"美观打印机",用于生成数据结构的一个美观视图.格式化工具会生成数据结构的一些表示,不仅可以由解释器正确地解析,而且便于人类阅读.输出尽可能放在一行上,分解为多行时则需要缩进. 以下实例用用到的data包含一下数据 data = [(1,{'a':'A','b':'B','c':'C','d':'D'}), (2,{'e':'E','f':'F','g':'G','h':'H', 'i':'I','j':'J',

在Python中使用M2Crypto模块实现AES加密的教程_python

 AES(英文:Advanced Encryption Standard,中文:高级加密标准),是一种区块加密标准.AES将原始数据分成多个4×4字节矩阵来处理,通过预先定义的密钥对每个字节矩阵中的每个字节进行异或.替换.移位以及线性变换操作来达到加密的目的.密钥长度可以是128,192或256比特.     下面是一个利用Python M2Crypto库,并使用aes_128_ecb算法进行加密和解密的例子.首先介绍一下几个关键的点: 1.iv(Initialization vector),即

Python中使用platform模块获取系统信息的用法教程_python

操作系统相关 system() : 操作系统类型(见例) version(): 操作系统版本 release(): 操作系统发布号, 例如win 7返回7, 还有如NT, 2.2.0之类. platform(aliased=0, terse=0): 操作系统信息字符串,扥与system()+win32_ver()[:3] win32_ver(release='', version='', csd='', ptype=''): win系统相关信息 linux_distribution(distna

Python中使用gzip模块压缩文件的简单教程_python

压缩数据创建gzip文件先看一个略麻烦的做法   import StringIO,gzip content = 'Life is short.I use python' zbuf = StringIO.StringIO() zfile = gzip.GzipFile(mode='wb', compresslevel=9, fileobj=zbuf) zfile.write(content) zfile.close() 但其实有个快捷的封装,不用用到StringIO模块   f = gzip.op

windows系统中基于WIFI的网络编程

问题描述 windows系统中基于WIFI的网络编程 想用C++写一个在windows系统中基于WIFI传输数据的网络小程序,请各位高手指点一下,应该从哪方面入手,开发流程是什么样的? 解决方案 什么也不用操心,wifi对于你的程序来说,是透明的,换一句话说,无线有线你的代码无需变化.在windows下,可以使用winsock编程实现通讯,如果是vc++,用mfc,可以用csocket类,具体google下,有现成的代码例子. 解决方案二: 就是winsock2而已不管网络是什么,用这个就对了,

Java网络编程从入门到精通(34):读写缓冲区中的数据---使用get和put方法按顺序读写单个数据

本文为原创,如需转载,请注明作者和出处,谢谢! 上一篇:Java网络编程从入门到精通(33):非阻塞I/O的缓冲区(Buffer)     对于缓冲区来说,最重要的操作就是读写操作.缓冲区提供了两种方法来读写缓冲区中的数据:get.put方法和array方法.而get.put方法可以有三种读写数据的方式:按顺序读写单个数据.在指定位置读写单个数据和读写数据块.除了上述的几种读写数据的方法外,CharBuffer类还提供了用于专门写字符串的put和append方法.在本文及后面的文章中将分别介绍这