Python-多线程编程(6寸)

整理文档很辛苦,赏杯茶钱您下走!

免费阅读已结束,点击下载阅读编辑剩下 ...

阅读已结束,您可以下载文档离线阅读编辑

资源描述

PythonPythonPythonPython多线程编程多线程编程多线程编程多线程编程1.引言2.线程和进程3.线程和Python4.thread模块5.threading模块6.生产者-消费者问题和Queue模块7.相关模块本节中,我们将探索在Python中,用多线程编程技术实现代码并行性的几种不同的方法。在前面几节中,我们将介绍进程与线程的区别。然后介绍多线程编程的概念。(已经熟悉多线程编程的读者可以直接跳到第1.3.5节)。本章的最后几节将演示在Python中如何使用threading和Queue模块来实现多线程编程。1.1引言在多线程(MT)编程出现之前,电脑程序的运行由一个执行序列组成,执行序列按顺序在主机的中央处理器(CPU)中运行。无论是任务本身要求顺序执行还是整个程序是由多个子任务组成,程序都是按这种方式执行的。即使子任务相互独立,互相无关(即,一个子任务的结果不影响其它子任务的结果)时也是这样。这样是不是有点不合逻辑?会不会想要并行运行这些相互独立的子任务呢?这样的并行处理可以大幅度地提升整个任务的效率。这就是多线程编程的目的。多线程编程对于某些任务来说,是最理想的。这些任务具有以下特点:它们本质上就是异步的,需要有多个并发事务,各个事务的运行顺序可以是不确定的,随机的,不可预测的。这样的编程任务可以被分成多个执行流,每个流都有一个要完成的目标。根据应用的不同,这些子任务可能都要计算出一个中间结果,用于合并得到最后的结果。运算密集型的任务一般都比较容易分隔成多个子任务,可以顺序执行或以多线程的方式执行。单线程处理多个外部输入源的的任务就不是那么容易了。这种编程任务如果不用多线程的方式处理,则一定要使用一个或多个计时器来实现。一个顺序执行的程序要从每个I/O(输入/输出)终端信道检查用户的输入时,程序无论如何也不能在读取I/O终端信道的时候阻塞。因为用户输入的到达是不确定的,阻塞会导致其它I/O信息的数据不能被处理。顺序执行的程序必须使用非阻塞I/O,或是带有计时器的阻塞I/O(这样才能保证阻塞只是暂时的)。由于顺序执行的程序只有一个线程在运行。它要保证它要做的多任务,不会有某个任务占用太多的时间,而且要合理地分配用户的响应时间。执行多任务的顺序执行的程序一般程序控制流程都很复杂,难以理解。使用多线程编程和一个共享的数据结构如Queue(本章后面会介绍的一种多线程队列数据结构),这种程序任务可以用几个功能单一的线程来组织:UserRequestThread:负责读取客户的输入,可能是一个I/O信道。程序可能创建多个线程,每个客户一个,请求会被放入队列中。RequestProcessor:一个负责从队列中获取并处理请求的线程,它为下面那种线程提供输出。ReplyThread:负责把给用户的输出取出来,如果是网络应用程序就把结果发送出去,否则就保存到本地文件系统或数据库中。把这种编程任务用多线程来组织可以降低程序的复杂度,并使得干净,有效和具有良好组织地程序结构实现变得可能。每个线程的逻辑都不会很复杂,因为它要做的事情很清楚。例如UserRequestThread只是从用户或某个数据源读取数据,放到一个队列中,等待其它线程进一步的处理,等等,每个线程都有自己明确的任务。你只要设计好每个线程要做什么,并把要做的事做好就可以了。对某些任务使用线程跟亨利福特制造汽车时使用的装配线模型有些相似。1.2线程和进程1.2.1什么是进程?计算机程序只不过是磁盘中可执行的,二进制(或其它类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过fork和spawn操作来完成其它的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(IPC),而不能直接共享信息。1.2.2什么是线程?线程(有时被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。它们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其它的线程运行,这叫做让步。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。实际上,在单CPU的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其它的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其它的线程共享运行的结果。当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果的不一致的问题。这叫做竞态条件(racecondition)。幸运的是,大多数线程库都带有一系列的同步原语,来控制线程的执行和数据的访问。另一个要注意的地方是,由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种“贪婪”的函数会让CPU的时间分配有所倾斜。导致各个线程分配到的运行时间可能不尽相同,不尽公平。1.3Python、线程和全局解释器锁1.3.1全局解释器锁(GIL)Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器中可以“运行”多个线程,但在任意时刻,只有一个线程在解释器中运行。对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程环境中,Python虚拟机按以下方式执行:1.设置GIL2.切换到一个线程去运行3.运行:a.指定数量的字节码指令b.线程主动让出控制(可以调用time.sleep(0))4.把线程设置为睡眠状态5.解锁GIL6.再次重复以上所有步骤在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)。编写扩展的程序员可以主动解锁GIL。不过,Python的开发人员则不用担心在这些情况下你的Python代码会被锁住。例如,对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其它的线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器(和GIL)。也就是说,I/O密集型的Python程序比计算密集型的程序更能充分利用多线程环境的好处。对源代码,解释器主循环和GIL感兴趣的人,可以看看Python/ceval.c文件。1.3.2退出线程当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用Python退出进程的标准方法,如sys.exit()或抛出一个SystemExit异常等。不过,你不可以直接“杀掉”(kill)一个线程。在下面一节中,我们将要讨论两个跟线程有关的模块。这两个模块中,我们不建议使用thread模块。这样做有很多原因,很明显的一个原因是,当主线程退出的时候,所有其它线程没有被清除就退出了。但另一个模块threading就能确保所有“重要的”子线程都退出后,进程才会结束。(我们等一会会详细说明什么叫“重要的”,请参阅守护线程的核心提示)。主线程应该是一个好的管理者,它要了解每个线程都要做些什么事,线程都需要什么数据和什么参数,以及在线程结束的时候,它们都提供了什么结果。这样,主线程就可以把各个线程的结果组合成一个有意义的最后结果。1.3.3在Python中使用线程在Win32和Linux,Solaris,MacOS,*BSD等大多数类Unix系统上运行时,Python支持多线程编程。Python使用POSIX兼容的线程,即pthreads。默认情况下,从源代码编译的(2.0及以上版本的)Python以及Win32的安装包里,线程支持是打开的。想要从解释器里判断线程是否可用,只要简单的在交互式解释器里尝试导入thread模块就行了,只要没出现错误就表示线程可用。importthread如果你的Python解释器在编译时,没有打开线程支持,导入模块会失败:importthreadTraceback(innermostlast):Filestdin,line1,in?ImportError:Nomodulenamedthread这种情况下,你就要重新编译你的Python解释器才能使用线程。你可以在运行配置脚本的时候,加上“--with-thread”参数。参考你的发布版的README文件,以获取如何编译支持线程的Python的相关信息。1.3.4没有线程支持的情况第一个例子中,我们会使用time.sleep()函数来演示线程是怎样工作的。time.sleep()需要一个浮点型的参数,来指定“睡眠”的时间(单位秒)。这就意味着,程序的运行会被挂起指定的时间。我们要创建两个“计时循环”。一个睡眠4秒种,一个睡眠2秒种,分别是loop0()和loop1()。(我们命名为“loop0”和“loop1”表示我们将有一个循环的序列)。如果我们像例1.1的onethr.py中那样,在一个进程或一个线程中,顺序地执行loop0()和loop1(),那运行的总时间为6秒。在启动loop0(),loop1(),和其它的代码时,也要花去一些时间,所以,我们看到的总时间也有可能会是7秒钟。例1.1单线程中运行的循环(onethr.py)在单线程中顺序执行两个循环。一定要一个循环结束后,另一个才能开始。总时间是各个循环运行时间之和。1#!/usr/bin/envpython23fromtimeimportsleep,ctime45defloop0():6print'startloop0at:',ctime()7leep(4)8print'loop0doneat:',ctime()910defloop1():11print'startloop1at:',ctime()12sleep(2)13print'loop1doneat:',ctime()1415defmain():16print'startingat:',ctime()17loop0()18loop1()19print'allDONEat:',ctime()2021if__name__=='__main__':22main()我们可以通过运行onethr.py来验证这一点,下面是运行的输出:$onethr.pystartingat:SunAug1305:03:342006startloop0at:SunAug1305:03:342006loop0doneat:SunAug1305:03:382006startloop1at:SunAug1305:03:382006loop1doneat:SunAug1305:03:402006allDONEat:SunAug1305:03:402006假定loop0()和loop1()里做的不是睡眠,而是各自独立的,不相关的运算,各自的运算结果到最后将会汇总成一个最终的结果。这时,如果能让这些计算并行执行的话,那不是可以减少总的运行时间吗?这就是我们现在要介绍的多线程编程的前提条件。1.3.5Python的threading模块Python提供了几个用于多线程编程的模块,包括thread,threading和Queue等。threa

1 / 45
下载文档,编辑使用

©2015-2020 m.777doc.com 三七文档.

备案号:鲁ICP备2024069028号-1 客服联系 QQ:2149211541

×
保存成功