Python多线程
同步和异步概念和区别
我们经常会看到同步、异步、阻塞、非阻塞四种调用方式以及它们的组合。那同步和异步的概念和区别又是什么呢?
同步(Sync)
同步的思想就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。 也就是说所有的操作都做完,才返回给用户。(会给人一种程序还在进行,卡死的感觉),事情需要一件一件的做,等前一件做完了才能做下一件事。
举例:B/S模式中的表单提交,具体过程是:客户端提交请求->等待服务器处理->处理完毕返回,在这个过程中客户端(浏览器)不能做其他事。
异步(Async)
异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
对于通知调用者的三种方式,具体如下:
状态
即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。
通知
当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。
回调
与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。
举例:B/S模式中的ajax请求,具体过程是:客户端发出ajax请求->服务端处理->处理完毕执行客户端回调,在客户端(浏览器)发出请求后,仍然可以做其他的事。
同步和异步的区别:
请求发出后,是否需要等待结果,才能继续执行其他操作。
多线程的动机
在多线程(MT)编程出现之前,程序的运行由一个执行序列组成,执行序列按顺序在主机的中央处理器(CPU)中运行。无论是任务本身要求顺序执行还是整个程序是由多个子任务组成,程序都是按照这种方式执行的,即便子任务相互独立,互相无关(即,一个子任务的结果不影响其他子任务的结果)时也是这样。多线程的目的就是要并行运行这项相互独立的子任务,这样的并行处理可以大幅度的提升整个任务的效率。
多线程对于某些任务是最理想的,这些任务的特点是:**它们本质上就是异步的,需要有多个并发事务,各个事务的运行程序可以是不确定的,随机的,不可预测的。**这样的编程任务可以被分为多个执行流,每个流都要有一个要完成的目标。根据应用的不同,这些子任务可能都要计算出一个中间结果,用于合并得到最后的结果。
使用多线程编程和一个共享的数据结构如Queue(一种多线程队列数据结构),这种程序任务可以用几个功能单一的线程来组织:
-
UserRequestThread:负责读取用户的输入,可能是一个I/O通道。程序可能创建多个线程
每个客户一个,请求会被放到队列中。
-
RequestProcessor:一个负责从队列中获取并处理请求的线程,它为下面那种线程提供输出。
-
ReplyThrea:负责把用户的输出取出来,如果是网络应用程序就把结果发送出去,否则就保存在本地系统或者数据库中。
把编程任务用多线程来组织可以降低程序的复杂度,并使得干净,有效和良好组织地程序结构的实现变得可能。并且每个线程的逻辑都不会很复杂,因为它要做的事很清楚。
线程和进程
什么是进程?
计算机程序只不过是磁盘中可执行的,二进制(或其他类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。进程(有时叫重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。OS管理在其上运行的所有进程,并为这些进程公平的分配时间。进程也可以通过fork和spawn操作来完成其它的任务。不过各个进程都有自己的内存空间,数据栈等,因此只能使用进程间通信(IPC),而不能直接共享信息。
什么是线程?
线程(有时也叫轻量级进程)和进程有点像,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。
线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(睡眠),让其他的线程运行,这叫做让步。
一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。
在单CPU的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其他的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其他的线程共享运行的结果。当然,这样的共享是存在危险的,如果多个线程共同访问同一片数据,则由于数据的访问顺序不一样,有可能造成结果不一致问题。这就是竞态条件(race condition)。但可以通过大多数线程库带有一系列的同步原语,来控制线程的执行和数据的访问。还有就是由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种"贪婪"的函数会让CPU的时间分配有所倾斜,导致各个线程分配的运行时间可能不尽相同,不尽公平。
Python线程和GIL
全局解释锁(GIL)
Python代码的执行是由Python虚拟机(也叫解释器主循环)来控制,Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单CPU系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地道理,虽然Python解释器中可以运行多个线程,但在任意时刻,只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程的环境下,Python虚拟机按以下方式执行:
-
设置GIL
-
切换到一个线程去运行
-
运行
a.指定数量的字节码指令,或者,
b.线程主动让出控制(可以调用time.sleep(0))
-
把线程设置为睡眠状态
-
解锁GIL
-
再次重复以上所有步骤
在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(因为在这期间内有Python的字节码被运行,所以不会做线程切换),编写扩展的程序员可以主动解锁GIL。
对于I/O密集型的Python程序比计算密集型程序更能充分利用多线程环境的好处。这是因为,对所有面向I/O的(会调用内建的OS的C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其它的线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。
GIL的一点源码
退出线程
当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用Python退出进程的标准方法,如sys.exit()或抛出一个SystemExit异常等。但是,我们不可以直接kill掉一个进程。
有两个和线程相关的模块thread和threading,但不建议直接使用thread模块,显而易见的一个原因是当主进程退出的时候,所有其它线程没有被清除就退出了。但另一个模块threading就能确保所有"重要的"子线程都退出后,进程才会结束。
而且,主线程应当作为一个好的管理者,它要了解每个线程都要做些什么事,线程都需要什么数据和参数,以及在线程结束的时候,它们都提供了什么结果。这样,主线程就可以把各个线程的结果组合成一个有意义的最后结果。
实际上Python提供了几个用于多线程编程的模块,包括thread,threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,而threading提供了更高级别,功能更强的线程管理功能。Queue模块允许用户创建一个用于多个线程之间共享数据的队列数据结构。
没有线程支持的情况
|
|
我们会使用time.sleep()函数来演示线程是怎样工作的。time.sleep()需要一个浮点型参数,来指定"睡眠"的时间(单位秒)。这就意味着,程序的运行会被挂起指定的时间。
我们要创建两个"计时循环"。一个睡眠4秒钟,一个睡眠2秒钟,分别是loop0()和loop1().(我们命名为"loop0"和"loop1"表示我们将有一个循环的序列)。
如果我们在一个进程或者一个线程中,顺序的执行loop0()和loop1(),那运行的总时间为6秒。在启动loop0(),loop1(),和其它的代码时,也要花去一些时间,所以,我们看到的总时间也有可能会是7秒钟。
假定loop0和loop1里做的不是睡眠,而是各自独立的,不相关的运算,各自的运算结果到最后将会汇总成一个最终的结果。这时,如果能让这些计算并行执行的话,那这样就会减少总的运行时间,这就是多线程编程的前提条件。
避免使用thread模块
不建议使用thread模块,首先是因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading模块出现冲突。其次,低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多。
另一个不要使用thread原因是,对于你的进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。而threading模块能确保重要的子线程退出后进程才退出。
只建议那些有经验的专家想访问线程的底层结构的时候,才使用thread模块。
thread模块和threading模块
thread模块
thread模块除了产生线程外,thread模块也提供了基本的同步数据结构锁对象(lock object,也叫原语锁,简单锁,互斥锁,互斥量,二值信号量),同步原语与线程的管理是密不可分的。
下表列出常用的线程函数以及LockType类型的锁对象的方法。
函数 | 描述 |
---|---|
thread模块函数 | |
start_new_thread(function,args,kwargs=None) | 产生一个新的线程,在新线程中用指定的参数和可选的kwargs来调用这个函数。 |
allocate_lock() | 分配一个LockType类型的锁对象 |
exit() | 让线程退出 |
LockType类型锁对象方法 | |
acquire(wait=None) | 尝试获得锁对象 |
locked() | 如果获取了锁对象返回True,否则返回False |
release() | 释放锁 |
start_new_thread()函数式thread模块的一个关键函数,它的语法与内建的apply()函数完全一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行,而是产生一个新的线程来运行这个函数。
这里我们执行的是和onethr.py中一样的循环,不同的是,这一次我们使用的是thread模块提供的多线程的机制。两个循环并发的执行(显然,短的那个先结束)。总的运行时间为最慢的那个线程的运行时间,而不是所有线程的运行时间之和。
|
|
这个程序的输出与之前的输出大不相同,之前是运行了6,7秒,而现在则是4秒,是最长的循环的运行时间和其它代码的时间之和。睡眠4秒和睡眠2秒是并发执行的,这样就使得总的运行时间被缩短了。loop1在loop0前面就结束了,而且程序多了一个sleep(6)的函数调用。为什么要加入sleep(6),这是因为如果没有让主线程停下来,那主线程就会运行下一条语句,显示"all done",然后就关闭了运行着的loop0()和loop1()的两个线程,退出了。
的确这里应该有更好的管理线程的办法,而不是在主线程里面做一个额外的延时6s的操作。也就是说,我们要写让主线程停下来等所有子线程结束之后再继续运行的代码,这也是线程需要同步的原因。用sleep()函数做线程的同步操作是不可靠的,因为如果有循环的执行时间不能事先确定的话,这可能会造成主线程过早或过晚退出,这就要用到锁了。
Python多进程
Python多进程和多线程参考
扩展阅读
Python中的multiprocessing和threading
Python 多线程 threading和multiprocessing模块
python多进程
python多线程
说明:如果想让python多线程,真正支持多核,那么应该 1、重写python编译器(官方cpython)如使用:PyPy解释器 2、调用C语言的链接库 。
而实际上,多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低 。
python协程
Python并行的最好策略应当是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。 。