Python中的进程、线程和协程
我们都知道计算机是由硬件和软件组成的。硬件中的CPU是计算机的核心,它承担计算机的所有任务。操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配、任务的调度。程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。现代操作系统,比如Mac OS X,Unix,Linux,Windows等,都是支持“多任务”的操作系统。
什么叫做“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边用浏览器上网,一边在听MP3,一边在用Word赶作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。
现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?
答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒......这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成。数据集则是程序在执行过程中所需要使用的资源。进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
举一例,说明进程,
想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。
在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减少了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源,只拥有在运行时必不可少的资源。但线程可以与同属于同一进程的其它线程共享所拥有的其他资源。
由于每个进程至少要干一件事,所以,一个进程至少有一个线程。多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。
同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,所以,多进程和多线程的程序的复杂度要远远高于单进程和单线程的程序。
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。CPU分给线程,即真正在CPU上运行的是线程。
资源分配给进程,同一进程的所有线程共享该进程的所有资源。线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。线程可与属于同一进程的其它线程共享进程所拥有的全部资源,但是其本身基本上不拥有系统资源,只拥有一点在运行中必不可少的信息(如程序计数器、一组寄存器和栈)。
如果要同时执行多个任务怎么办?有两种解决方案:
一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。还有一种方法是启动一个进程,在一个进程中启动多个线程,这样,多个线程也可以一块执行多个任务。当然,还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。
总结一下就是,多任务的实现有3种方式:
多进程模式;
多线程模式;
多进程+多线程模式;
并行与并发
并行处理(Parallel Processing)是计算机系统中能同时执行两个或者更多个处理的一种计算方法。并行处理可以同时工作于同一程序的不同方面,并行处理的主要目的是节省大型和复杂问题的解决时间。
并发处理(Concurrency Processing)是指一个时间段中有几个程序多处于已经启动运行到运行完毕之间,而且这几个程序都是在同一CPU上运行,但任意时刻点上只有一个程序在CPU上运行。
并发的关键在于你有处理多个任务的能力,不一定同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集。
并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机。如果串行,一个队列使用一台咖啡机,那么哪怕前面那个人便秘了去厕所呆半天,后面的人也只能等着他回来才能去接咖啡,这效率无疑是最低的。
并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)CPU执行,如果可以就说明是并行,而并发是多个线程被(一个)CPU轮流切换着执行。
同步与异步、阻塞与非阻塞
同步和异步关注的是消息通信机制(synchronous communication/asynchronous communication)。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是,一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。调用者可以继续后续的操作。换句话说,当一个异步过程调用发出之后,调用者不会立刻得到结果。而是在"调用"发出之后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。
举个通俗的例子:
你打电话问书店老板有没有《数据挖掘》这本书,如果是同步通信机制,书店老板会说,你稍等,“我查一下”,然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后等查好了,他会主动打电话给你。在这里老板通过"回电"这种方式来回调。
阻塞与非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子,
你打电话问书店老板有没有《数据挖掘》这本书,如果是阻塞式调用,你会一直把自己"挂起",直到得到这本书有没有的结果。如果是非阻塞调用,你不管老板有没有告诉你,你自己先一边玩去了,当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
再举一个通俗的例子,
出场人物:爱喝茶的老张同志,水壶两把(普通水壶,简称水壶。会响的水壶,简称响水壶)。
1.老张把水壶放到火上,立等水开。(同步阻塞)
2.老张觉得自己有点傻。老张把水壶放到火上,去客厅看电视,时不时地去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是升级了装备,买了把会响笛的水壶。水开之后,能大声发出滴滴的噪音。
3.老张把响水壶放到火上,立等水开。(异步阻塞)
4.老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
所谓同步异步,只是对于水壶而言。普通水壶,同步。响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶不能及的。同步只能让调用者去轮询自己(情况2),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老站而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中,老张就是阻塞的,媳妇喊他都不知道。虽然情况3中,响水壶是异步的,可对于立等的老张没有太大的意义。所以,一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
在处理IO的时候,阻塞和非阻塞都是同步IO。
只有使用了特殊的API,才是异步IO。
再看POSIX(可移植操作系统接口,Portable Operating System Interface of Unix)对这两个术语的定义:
同步I/O操作:导致请求进程阻塞,直到I/O操作完成;
异步I/O操作:不导致请求进程阻塞。
总结一下同步和异步,阻塞和非阻塞,
阻塞
程序未得到所需计算资源时被挂起的状态。
程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。
常见的阻塞形式有:网络I/O阻塞、磁盘I/O阻塞、用户输入阻塞等。
阻塞是无处不在的,包括CPU切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。(如果是多核CPU,则正在执行上下文切换操作的核不可被利用。)
非阻塞
程序在等待某操作过程中,自身不被阻塞,可以继续执行干别的事情,则称该程序在该操作上是非阻塞的。
非阻塞的存在时因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。
线程
线程(有时被称为轻量级进程),所有的线程运行在同一个进程中,共享相同的运行环境。它们可以想象成是在主进程或主线程中并行运行的“迷你进程”。
1.1线程状态
线程有5种状态,状态转换的过程如下图所示,
简单地说,线程可以分成开始,顺序执行和结束三种状态,它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其它的线程运行,这叫做让步。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。
当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果不一致的问题。这叫做竞态条件(race condition)。
线程一般都是并发执行的,不过在单CPU的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其它的线程去运行。由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种“贪婪”的函数会让CPU的时间分配有所倾斜。导致各个线程分配到的运行时间可能不尽相同,不尽公平。
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
1.2线程同步(锁)
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
我们来看一个具体的例子,在某一进程中,内存空间中有一个变量对象的值为num=8,假如某一时刻有多个线程需要同时使用这个对象,而这些线程需要实现不同的功能,线程A需要将num减1后再使用,线程B需要将num加1后再使用,而线程C需要使用num原来的值8。由于这三个线程都是共享存储num值的内存空间的,并且这三个线程是可以同时并发执行的,当三个线程同时对num操作时,因为num只有一个,所以肯定会存在不同的操作顺序,想象一下下面这样的过程:
第一步:线程A修改了num的值为7;
第二步:线程C不知道num的值已经发生了改变,直接调用了num的值7;
第三步:线程B对num值加1,此时num值变为8;
第四步:线程B使用了num值8;
第五步:线程A使用了num值8;
因为num只有一个,而三个操作都针对同一个num进行,所以上面的操作过程是完全有可能的,而原来线程A、B、C想要使用的num值应该分别为7,9,8,这里却变成了8,8,7。试想一下,如果这三个线程的操作对整个程序的执行是至关重要的,会造成什么样的后果?
因此出于程序稳定运行时的考虑,对于线程需要调用内存中的共享数据时,我们就需要为线程加锁。
锁有两种状态---锁定和未锁定。每当一个线程,比如"set",要访问共享数据时,必须先获得锁定。如果已经有别的线程获得锁定了,比如"print",那么就让线程"set"暂停,也就是同步阻塞。等到线程"print"访问完毕,释放锁之后,再让线程"set"继续。经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
线程与锁的交互如下图所示:
1.3线程通信(条件变量)
然而,还有另外一种尴尬的情况:列表并不是一开始就有的。而是通过线程"create"创建的。如果"set"或者"print"在"create"还没有运行的时候就访问列表,将会出现一个异常。使用锁可以解决这个问题,但是"set"和"print"将需要一个无限循环---他们不知道"create"什么时候会运行,让"create"在运行后通知"set"和"print"显然是一个更好的解决方案。于是,引入了条件变量。
条件变量允许线程,比如"set"和"print"在条件不满足的时候(列表为None时)等待,等到条件满足的时候(列表已经创建)发出一个通知,告诉"set"和"print"条件已经有了,你们该起床干活了。然后"set"和"print"才继续运行。
线程与条件变量的交互如下图所示:
1.4线程运行和阻塞的状态转换
最后,看看线程运行和阻塞状态的转换。
阻塞有三种情况:
同步阻塞:是指处于竞争锁定的状态,线程请求锁定时进入这个状态,一旦成功获得锁定又恢复到运行状态;
等待阻塞:是指等待其他线程通知的状态,线程获得条件锁定后,调用"等待"将进入这个状态。一旦其他线程发出通知,线程将进入同步阻塞状态,再次竞争条件锁定;
其他阻塞:是指调用time.sleep(),anotherthread.join()或者等待IO时的阻塞,这个状态下线程不会释放已获得的锁定。
thread和threading
Python的标准库提供了两个模块:thread和threading,thread是低级模块,以低级、原始的方式来处理和控制线程。threading是高级模块,对thread进行了封装,提供了更方便的api来处理线程。
thread
#-*-coding:UTF-8-*-
import thread
import time
# 一个用于在线程中执行的函数
def func():
for i in range(5):
print 'func'
time.sleep(1)
# 结束当前线程
# 这个方法与thread.exit_thread()等价
thread.exit() # 当func返回时,线程同样会结束
# 启动一个线程,线程立即开始运行
# 这个方法与thread.start_new_thread()等价
# 第一个参数是方法,第二个参数是方法的参数
thread.start_new(func, ()) # 方法没有参数时需要传入空tuple
# 创建一个锁(LockType,不能直接实例化)
# 这个方法与thread.allocate_lock()等价
lock = thread.allocate()
# 判断锁是锁定状态还是释放状态
print lock.locked()
# 锁通常用于控制对共享资源的访问
count = 0
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
if lock.acquire():
count += 1
# 释放锁
lock.release()
# thread模块提供的线程都将在主线程结束后同时结束
time.sleep(6)
thread模块提供的其他方法:
thread.interrupt_main():在其他线程中终止主线程。
thread.get_ident():获得一个代表当前线程的魔法数字,常用于从一个字典中获得线程相关的数据。这个数字本身没有任何含义,并且当线程结束后会被新线程复用。
thread还提供了一个ThreadLocal类用于管理线程相关的数据,名为thread._local,threading中引用了这个类。
由于thread提供的线程功能不多,无法在主线程结束后继续运行,不提供条件变量等等原因,一般不使用thread模块,这里不多介绍了。
threading
绝大多数情况下,我们只需要使用threading这个高级模块。threading基于Java的线程模型设计。锁(Lock)和条件变量(Condition)在Java中是对象的基本行为(每一个对象都自带了锁和条件变量),而在Python中则是独立的对象。
threading模块提供的常用方法:
threading.currentThread():返回当前的线程变量;
threading.enumerate():返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
In [1]: import threading
In [2]: threading.currentThread()
Out[2]: <_MainThread(MainThread, started 4344)>
In [3]: threading.enumerate()
Out[3]:
[<_MainThread(MainThread, started 4344)>,
<Thread(Thread-4, started daemon 1136)>,
<ParentPollerWindows(Thread-3, started daemon 3724)>,
<Heartbeat(Thread-5, started daemon 868)>,
<HistorySavingThread(IPythonHistorySavingThread, started 4556)>]
In [4]: threading.activeCount()
Out[4]: 5
threading模块提供的类:
Thread,Lock,Rlock,Condition,[Bounded] Semaphore,Event,Timer,local。
1.threading.Thread类
Thread是线程类,有两种使用方法,直接传入要运行的方法或从Thread继承并覆盖run()。
构造方法:Thread(group=None,target=None,name=None,args=(),kwargs={})
group:线程组,目前还没有实现,库引用中提示必须是None;
target:要执行的方法;
name:线程名;
args/kwargs:要传入方法的参数。
#-*-coding: UTF-8-*-
import threading
# 方法1:将要执行的方法作为参数传给Thread的构造方法
def func():
print 'func() passed to Thread'
t = threading.Thread(target=func)
t.start()
#输出
func() passed to Thread
# 方法2:从Thread继承,并重写run()
class MyThread(threading.Thread):
def run(self):
print 'MyThread extended from Thread'
t = MyThread()
t.start()
#输出
MyThread extended from Thread
启动一个线程,就是把一个函数传入并创建Thread实例,然后调用start()开始执行:
import time, threading
# 新线程执行的代码:
def loop():
print 'thread %s is running...' % threading.current_thread().name
n = 0
while n < 5:
n = n + 1
print 'thread %s >>> %s' % (threading.current_thread().name, n)
time.sleep(1)
print 'thread %s ended.' % threading.current_thread().name
print 'thread %s is running...' % threading.current_thread().name
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print 'thread %s ended.' % threading.current_thread().name
执行结果如下:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
由于任何进程,默认会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫做MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字,Python就会自动给线程命名为Thread-1,Thread-2......
Threading用于提供线程相关的操作。线程是应用程序中工作的最小单元,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
threading模块建立在thread模块之上。thread模块以低级、原始的方式来处理和控制线程,而threading模块通过对thread进行二次封装,提供了更方便的api来处理线程。
Thread实例方法:
t.start():激活/启动线程;
t.getName():获取线程的名称;
t.setName():设置线程的名称;
t.name:获取或设置线程的名称;
t.is_alive():判断线程是否为激活(运行)状态,正在运行指启动后,终止前;
t.isAlive():判断线程是否为激活状态;
t.setDaemon(bool):将线程设置为守护/后台线程(默认:False),通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之前设置。
如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止。
如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止。初始值从创建该线程的线程继承。
A thread can be flagged as a "daemon thread". The significance of this flag is that the entire Python program exits when only daemon threads are left.The initial value is inherited from the creating thread. The flag can be set throught the daemon property.
线程可以被标识为“Daemon线程”,Daemon线程表明整个Python主程序,当在只剩下daemon线程运行时才可以退出。该属性值继承自父线程,可以通过setDaemon()函数设定该值。
Some threads do background tasks, like sending keepalive packets, or performing periodic garbage collection, or whatever. These are only useful when the main program is running, and it's okay to kill them off once the other, non-daemon, threads have exited.
Without daemon threads, you'd have to keep track of them, and tell them to exit, before your program can completely quit. By setting them as daemon threads, you can let them run and forget about them, and when your program quits, any daemon threads are killed automatically.
Note:Daemon threads are abruptly stopped at shutdown.Their resources(such as open files, database transactions, etc.)may not be released properly. If you want your threads to stop gracefully, make them non-daemonic and use a suitable signalling mechanism such as an Event.
注意:Daemon线程(守护线程)会被粗鲁的直接结束,它所使用的资源(已打开文件、数据库事务等)无法被合理的释放。因此,如果需要线程被优雅的结束,请设置为非Daemon线程,并使用合理的信号方法,如事件Event。
daemon:A boolean value indicating whether this thread is a daemon thread(True) or not(False).This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread, the main thread is not a daemon thread and therefore all the threads created in the main thread default to daemon = False.
The entire Python pragrams exists when no alive non-daemon threads are left.
Python主程序当且仅当不存在非Daemon线程存活时退出。即:主程序等待所有非Daemon线程结束后才退出,且退出时会自动结束(很粗鲁的结束)所有Daemon线程。亦可理解为:Daemon设置为子线程是否随主线程一起结束,默认为False。如果要随主线程一起结束需要设置为True。
Daemons are only useful when the main program is running, and it is okay to kill them off once the other non-daemon threads have existed. Without daemon threads, we have to keep track of them, and tell them to exit, before our program can completely quit. By setting them as daemon threads, we can let them run and forget about them, and when our programs quits, any daemons threads are killed automatically.
Daemon线程当且仅当主线程运行时有效,当其他非Daemon线程结束时,可自动杀死所有Daemon线程。如果没有Daemon线程,则必须手动跟踪这些线程,在程序结束前手动结束这些线程。通过设置线程为Daemon线程,则可以放任它们运行,并遗忘它们,当主程序结束时,这些Daemon线程将自动被杀死。
对线程的Daemon的误解,下述描述是错误的:设置线程为守护线程,主线程退出后,子线程仍运行直到任务结束。
Daemon守护进程
Daemon程序是一直运行的服务端程序,又称为守护进程。
通常在系统后台运行,没有控制终端,不与前台交互,Daemon程序一般作为系统服务使用。
Daemon是长时间运行的进程,通常在系统启动后就运行,在系统关闭时才结束。一般说Daemon程序在后台运行,是因为它没有控制终端,无法和前台的用户进行交互。Daemon程序一般都作为服务程序使用,等待客户端程序与它通信。我们也把运行的Daemon程序称作守护进程。
Daemon程序实现方法
编写Daemon程序有一些基本的规则,以避免不必要的麻烦。
1.首先是程序运行后调用fork,并让父进程退出。子进程获得一个新的进程ID,但继承了父进程的进程组ID。
2.调用setsid创建一个新的session,使自己成为新session和新进程组的leader,并使进程没有控制终端(tty)。
3.改变当前工作目录至根目录,以免影响可加载文件系统。或者也可以改变到特定的目录。
4.设置文件创建mask为0,避免创建文件时权限的影响。
5.关闭不需要的打开文件描述符。因为Daemon程序在后台执行,不需要与终端交互,通常就关闭STDIN,STDOUT和STDERR。其它根据实际情况处理。
另一个问题是Daemon程序不能和终端交互,也就无法使用printf方法输出信息了。我们可以使用syslog机制来实现信息的输出,方便程序的调试。在使用syslog前需要首先启动syslog程序。
t.isDaemon():判断是否为守护线程;
t.ident:获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后,该属性才有效,否则它只返回None。
t.join([timeout]):阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数)。逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义。timeout参数是可选的,代表线程运行的最大时间,即如果超过这个时间,不管这个线程有没有执行完毕都会被回收,然后主线程或函数会接着执行。
t.run():线程被CPU调度之后,自动执行线程对象的run方法。
In [1]: threading.currentThread()
...:
Out[1]: <_MainThread(MainThread, started 4344)>
In [2]: type(threading.currentThread())
Out[2]: threading._MainThread
In [3]: threading.currentThread().getName()
Out[3]: 'MainThread'
In [4]: threading.currentThread().ident
Out[4]:24324
一个使用join()的例子:
import threading
import time
def context(tJoin):
print 'in threadContext'
tJoin.start()
#将阻塞threadContext直到threadJoin终止
tJoin.join()
#threadJoin终止后继续执行
print 'out threadContext.'
def join():
print 'in threadJoin'
time.sleep(1)
print 'out threadJoin'
tJoin = threading.Thread(target=join)
tContext = threading.Thread(target=context,args=(tJoin,))
tContext.start()
运行结果:
in threadContext
in threadJoin
out threadJoin
out threadContext.
2.threading.Lock类
Lock(指令锁)是可用的最低级的同步指令。Lock处于锁定状态时,不被特定的线程拥有。Lock包含两种状态---锁定和非锁定,以及两个基本的方法。可以认为Lock有一个锁定池,当线程请求锁定时,将线程至于池中,直到获得锁定后出池。池中的线程处于状态图中的同步阻塞状态。
构造方法:Lock()
实例方法:
acquire(timeout):使线程进入同步阻塞状态,尝试获得锁定。
release():释放锁。使用前线程必须已经获得锁定,否则将抛出异常。
import threading
import time
data = 0
lock = threading.Lock()
def func():
global data
print '%s acquire lock ...' %threading.currentThread().getName()
# 调用acquire([timeout])时,线程将一直阻塞,
# 直到获得锁定或者直到timeout秒后(timeout参数可选)。
# 返回是否获得锁。
if lock.acquire():
print '%s get the lock.' %threading.currentThread().getName()
data += 1
time.sleep(2)
print '%s release lock...' %threading.currentThread().getName()
# 调用release()将释放锁
lock.release()
t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t3 = threading.Thread(target=func)
t1.start()
t2.start()
t3.start()
#输出
Thread-13 acquire lock ...
Thread-13 get the lock.
Thread-14 acquire lock ...
Thread-15 acquire lock ...
Thread-13 release lock...
Thread-14 get the lock.
Thread-14 release lock...
Thread-15 get the lock.
Thread-15 release lock...
3.threading.RLock类
RLock(可重入锁)是一个可以被同一个线程请求多次的同步指令。RLock使用了"拥有的线程"和"递归等级"的概念,处于锁定状态时,RLock被某个线程拥有。拥有RLock的线程可以再次调用acquire(),释放锁时需要调用release()相同次数。
可以认为RLock包含一个锁定池和一个初始值为0的计数器,每次成功调用acquire()/release(),计数器将+1/-1,为0时,锁处于未锁定状态。
构造方法:RLock()
实例方法:
acquire([timeout])/release():跟Lock差不多。
import threading
import time
rlock = threading.RLock()
def func():
#第一次请求锁定
print '%s acquire lock...' %threading.currentThread().getName()
if rlock.acquire():
print '%s get the lock.' %threading.currentThread().getName()
time.sleep(2)
#第二次请求锁定
print '%s acquire lock again...' %threading.currentThread().getName()
if rlock.acquire():
print '%s get the lock again.' %threading.currentThread().getName()
time.sleep(2)
#第一次释放锁
print '%s release lock...' %threading.currentThread().getName()
rlock.release()
time.sleep(2)
#第二次释放锁
print '%s release lock again....' %threading.currentThread().getName()
rlock.release()
t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t3 = threading.Thread(target=func)
t1.start()
t2.start()
t3.start()
#输出
Thread-19 acquire lock...
Thread-19 get the lock.
Thread-20 acquire lock...
Thread-21 acquire lock...
Thread-19 acquire lock again...
Thread-19 get the lock again.
Thread-19 release lock...
Thread-19 release lock again....
Thread-20 get the lock.
Thread-20 acquire lock again...
Thread-20 get the lock again.
Thread-20 release lock...
Thread-20 release lock again....
Thread-21 get the lock.
Thread-21 acquire lock again...
Thread-21 get the lock again.
Thread-21 release lock...
Thread-21 release lock again....
In [ ]:
threading.RLock和threading.Lock的区别
RLock允许在同一线程内被多次acquire,而Lock却不允许这种情况。如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的锁。
>>> import threading
>>> lock = threading.Lock() #Lock对象
>>> lock.acquire()
True
>>> lock.acquire()#长时间没有反应,产生了死锁。
>>> import threading
>>> rlock = threading.RLock()#RLock对象
>>> rlock.acquire()
True
>>> rlock.acquire() #在同一线程内,程序不会堵塞
1
>>> rlock.release()
>>> rlock.release()
>>>
4.threading.Condition类
Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足,则wait。如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程。其他处于wait状态的线程接到通知后会重新判断条件。不断地重复这一过程,从而解决复杂的同步问题。
Condition(条件变量)通常与一个锁关联,因为条件变量总是和mutex(也就是Python中的Lock类对象)一起使用。需要在多个Conditions中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则它将自己生成一个RLock实例。可以对Condition对象调用acquire()和release()方法,以控制潜在的Lock对象。
可以认为,除了Lock带有的锁定池外,Condition还包含一个等待池,池中的线程处于状态图中的等待阻塞状态,直到另一个线程调用notify()/notifyall()通知,得到通知后,线程进入锁定池等待锁定。
构造方法:Condition([lock/rlock])
实例方法:
acquire([timeout])/release():调用关联的锁的相应方法,获得/释放锁。
wait([timeout]):调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用前,线程必须已获得锁定,否则将抛出异常。
notify():调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池)。其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前,线程必须已获得锁定,否则将抛出异常。
notifyAll():调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前,线程必须已获得锁定,否则将抛出异常。
例子,是很常见的生产者/消费者模式,
import threading
import time
#商品
product = None
#条件变量
con = threading.Condition()
#生产者方法
def produce():
global product
if con.acquire():
while True:
if product is None:
print 'produce...'
product = 'anything'
#通知消费者,商品已经生成
con.notify()
#等待通知
con.wait()
time.sleep(2)
#消费者方法
def consume():
global product
if con.acquire():
while True:
if product is not None:
print 'consume...'
product = None
#通知生产者,商品已经没了
con.notify()
#等待通知
con.wait()
time.sleep(2)
t1 = threading.Thread(target=produce)
t2 = threading.Thread(target=consume)
t2.start()
t1.start()
#输出
produce...
consume...
produce...
consume...
produce...
consume...
produce...
consume...
produce...
consume...
produce...
consume...
produce...
consume...
produce...
consume...
produce...
例子二:生产者消费者模型
import threading
import time
condition = threading.Condition()
products = 0
class Producer(threading.Thread):
def run(self):
global products
while True:
if condition.acquire():
if products < 10:
products += 1
print "Producer(%s):deliver one, now products:%s" %(self.name, products)
condition.notify()#不释放锁定,因此需要下面一句
condition.release()
else:
print "Producer(%s):already 10, stop deliver, now products:%s" %(self.name, products)
condition.wait() #自动释放锁定
time.sleep(2)
class Consumer(threading.Thread):
def run(self):
global products
while True:
if condition.acquire():
if products > 1:
products -= 1
print "Consumer(%s):consume one, now products:%s" %(self.name, products)
condition.notify()
condition.release()
else:
print "Consumer(%s):only 1, stop consume, products:%s" %(self.name, products)
condition.wait();
time.sleep(2)
if __name__ == "__main__":
for p in range(0, 2):
p = Producer()
p.start()
for c in range(0, 3):
c = Consumer()
c.start()
#输出
Producer(Thread-1):deliver one, now products:1
Producer(Thread-2):deliver one, now products:2
Consumer(Thread-3):consume one, now products:1
Consumer(Thread-4):only 1, stop consume, products:1
Consumer(Thread-5):only 1, stop consume, products:1
Producer(Thread-1):deliver one, now products:2
Producer(Thread-2):deliver one, now products:3
Consumer(Thread-3):consume one, now products:2
Consumer(Thread-4):consume one, now products:1
Consumer(Thread-4):only 1, stop consume, products:1
Consumer(Thread-5):only 1, stop consume, products:1
Producer(Thread-1):deliver one, now products:2
Consumer(Thread-3):consume one, now products:1
Producer(Thread-2):deliver one, now products:2
Consumer(Thread-4):consume one, now products:1
Consumer(Thread-4):only 1, stop consume, products:1
Consumer(Thread-5):only 1, stop consume, products:1
......
现在写个捉迷藏的游戏来具体介绍threading.Condition的基本使用。假如这个游戏由两个人来玩,一个藏(Hider),一个找(Seeker)。游戏的规则如下:1.游戏开始之后,Seeker先把自己眼睛蒙上,蒙上眼睛后,就通知Hider;2.Hider接收通知后,开始找地方将自己藏起来,藏好之后,再通知Seeker可以开始找了;3.Seeker接收到通知之后,就开始找Hider。Hider和Seeker都是独立的个体,在程序中用两个独立的线程来表示,在游戏过程中,两者之间的行为有一定的时序关系,我们通过Condition来控制这种时序关系。
import threading, time
class Seeker(threading.Thread):
def __init__(self, cond, name):
super(Seeker, self).__init__()
self.cond = cond
self.name = name
def run(self):
time.sleep(1) #确保先运行Hider中的方法
self.cond.acquire()
print self.name + ': 我已经把眼睛蒙上了'
self.cond.notify()
self.cond.wait()
print self.name + ': 我找到你了~-~'
self.cond.notify()
self.cond.wait()
class Hider(threading.Thread):
def __init__(self, cond, name):
super(Hider, self).__init__()
self.cond = cond
self.name = name
def run(self):
self.cond.acquire()
self.cond.wait() #释放对琐的占用,同时线程挂起在这里,直到被notify并重新占有锁
print self.name + ': 我已经藏好了,你快来找我吧'
self.cond.notify()
self.cond.wait()
self.cond.release()
print self.name + ': 被你找到了,哎~~~'
cond = threading.Condition()
seeker = Seeker(cond, 'seeker')
hider = Hider(cond, 'hider')
seeker.start()
hider.start()
#输出
seeker: 我已经把眼睛蒙上了
hider: 我已经藏好了,你快来找我吧
seeker: 我找到你了~-~
hider: 被你找到了,哎~~~
import threading
alist = None
condition = threading.Condition()
def doSet():
if condition.acquire():
while alist is None:
condition.wait()
for i in range(len(alist))[::-1]:
alist[i] = 1
condition.release()
def doPrint():
if condition.acquire():
while alist is None:
condition.wait()
for i in alist:
print i,
condition.release()
def doCreate():
global alist
if condition.acquire():
if alist is None:
alist = [0 for i in range(10)]
condition.notifyAll()
condition.release()
tset = threading.Thread(target=doSet,name='tset')
tprint = threading.Thread(target=doPrint,name='tprint')
tcreate = threading.Thread(target=doCreate,name='tcreate')
tset.start()
tprint.start()
tcreate.start()
#输出
1 1 1 1 1 1 1 1 1 1
5.threading.Event类
Event(事件)是最简单的线程通信机制之一:一个线程通知事件,其他线程等待事件。类似于一个线程向其它多个线程发号命令的模式,其它线程都会持有一个threading.Event的对象,这些线程都会等待这个事件的"发生",如果此事件一直不发生,那么这些线程将会阻塞,直至事件的"发生"。
Event内置了一个初始为False的标志,当调用set()时设为True,调用clear()时重置为False。wait()将阻塞线程至等待阻塞状态。
Event其实就是一个简化版的Condition。Event没有锁,无法使线程进入同步阻塞状态。
构造方法:Event()
实例方法:
isSet():当内置标志为True时返回True;
set():将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态;
clear():将标志设为False;
wait([timeout]):如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()。
import threading
import time
event = threading.Event()
def func():
#等待事件,进入等待阻塞状态
print '%s wait for event...' %threading.currentThread().getName()
event.wait()
#收到事件后,进入运行状态
print '%s recv event.' %threading.currentThread().getName()
t1 = threading.Thread(target=func)
t2 = threading.Thread(target=func)
t1.start()
t2.start()
time.sleep(2)
#发送事件通知
print 'MainThread set event.'
event.set()
#输出
Thread-8 wait for event...
Thread-9 wait for event...
MainThread set event.
Thread-8 recv event.
Thread-9 recv event.
考虑这样一种应用场景(仅仅作为说明),例如,我们有多个线程从Redis队列中读取数据来处理,这些线程都要尝试去连接Redis的服务。一般情况下,如果Redis连接不成功,在各个线程的代码中,都会去尝试重新连接。如果,我们想要在启动时确保Redis服务正常,才让那些工作线程去连接Redis服务器,那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作:主线程中会去尝试连接Redis服务,如果正常的话,触发事件,各工作线程会尝试连接Redis服务。
实现代码如下,
import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,format='(%(threadName)s) %(message)s')
def worker(event):
logging.debug('Waiting for redis ready...')
event.wait()
logging.debug('redis ready, and connect to redis server and do some work [%s]',time.ctime())
time.sleep(1)
readis_ready = threading.Event()
t1 = threading.Thread(target=worker, args=(readis_ready,), name='t1')
t1.start()
t2 = threading.Thread(target=worker, args=(readis_ready,), name='t2')
t2.start()
time.sleep(1)
logging.debug('first of all, check redis server, make sure it is OK, and then trigger the redis ready event')
time.sleep(3) # simulate the check progress
readis_ready.set()
#输出
(t1) Waiting for redis ready...
(t2) Waiting for redis ready...
(MainThread) first of all, check redis server, make sure it is OK, and then trigger the redis ready event
(t2) redis ready, and connect to redis server and do some work [Wed May 23 11:03:11 2018]
(t1) redis ready, and connect to redis server and do some work [Wed May 23 11:03:11 2018]
t1和t2线程开始的时候都会阻塞在等待redis服务器启动的地方,一旦主线程确定了redis服务器已经正常启动,那么会触发redis_ready事件,各个工作线程就会去连接redis去做相应的工作。
threading.Event的wait方法还接受一个超时参数,默认情况下如果事件一直没有发生,wait方法会一直阻塞下去,而加入这个超时参数之后,如果阻塞时间超过这个参数设定的值之后,wait方法会返回。对应于上面的应用场景,如果Redis服务器一致没有启动,我们希望子线程能够打印一些日志来不断地提醒我们当前没有一个可以连接的Redis服务,我们就可以通过设置这个超时参数来达成这样的目的:
import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,format='(%(threadName)s) %(message)s')
def worker(event):
while not event.is_set():
logging.debug('Waiting for redis ready...')
event.wait(1)
logging.debug('redis ready, and connect to redis server and do some work [%s]',time.ctime())
time.sleep(1)
readis_ready = threading.Event()
t1 = threading.Thread(target=worker, args=(readis_ready,), name='t1')
t1.start()
t2 = threading.Thread(target=worker, args=(readis_ready,), name='t2')
t2.start()
time.sleep(1)
logging.debug('first of all, check redis server, make sure it is OK, and then trigger the redis ready event')
time.sleep(3) # simulate the check progress
readis_ready.set()
与前面的无限阻塞版本唯一的不同就是,我们在工作线程中加入了一个while循环,直到redis_ready事件触发之后才会结束循环,wait方法调用会在1秒的超时后返回,这样,我们就可以看到各个工作线程在系统启动的时候等待redis_ready的同时,会记录一些状态信息。以下是这个程序的运行结果:
(t1) Waiting for redis ready...
(t2) Waiting for redis ready...
(t2) Waiting for redis ready...
(MainThread) first of all, check redis server, make sure it is OK, and then trigger the redis ready event
(t1) Waiting for redis ready...
(t2) Waiting for redis ready...
(t1) Waiting for redis ready...
(t1) Waiting for redis ready...
(t2) Waiting for redis ready...
(t1) Waiting for redis ready...
(t1) redis ready, and connect to redis server and do some work [Wed May 23 11:11:57 2018]
(t2) redis ready, and connect to redis server and do some work [Wed May 23 11:11:57 2018]
这样,我们就可以在等待Redis服务启动的同时,看到工作线程里正在等待的情况。
6.threading.Semaphore类/BoundedSemaphore函数
Semaphore(信号量)是计算机科学史上最古老的同步指令之一。Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1。计数器不能小于0。当计数器为0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。
基于这个特点,Semaphore经常用来同步一些有"访客上限"的对象,比如连接池。
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
构造方法:Semaphore(value=1):value是计数器的初始值。
实例方法:
acquire([timeout]):请求Semaphore。如果计数器为0,将阻塞线程至同步阻塞状态,并等待其他线程调用release()方法以使计数器为正。否则,将计数器-1并立即返回。这个过程有严格的互锁机制控制,以保证如果有多条线程正在等待解锁,release()调用只会唤醒其中一条线程。唤醒哪一条是随机的。
release():释放Semaphore,将计数器+1,如果使用BoundedSemaphore,还将进行释放次数检查。release()方法不检查进程是否已获得Semaphore。
import threading
import time
#计数器初值为2
semaphore = threading.Semaphore(2)
def func():
#请求Semaphore,成功后计数器-1。计数器为0时阻塞。
print '%s acquire semaphore' %threading.currentThread().getName()
if semaphore.acquire():
print '%s get semaphore' %threading.currentThread().getName()
time.sleep(4)
#释放Semaphore,计数器+1
print '%s release semaphore' %threading.currentThread().getName()
semaphore.release()
t1 = threading.Thread(target=func, name='t1')
t2 = threading.Thread(target=func, name='t2')
t3 = threading.Thread(target=func, name='t3')
t4 = threading.Thread(target=func, name='t4')
t1.start()
t2.start()
t3.start()
t4.start()
time.sleep(2)
#没有获得semaphore的主线程也可以调用release
#若使用BoundedSemaphore,t4释放semaphore时将抛出异常
print 'MainThread release semaphore without acquire'
semaphore.release()
#输出
t1 acquire semaphore
t1 get semaphore
t2 acquire semaphore
t2 get semaphore
t3 acquire semaphore
t4 acquire semaphore
MainThread release semaphore without acquire
t3 get semaphore
t1 release semaphore
t4 get semaphore
t2 release semaphore
t3 release semaphore
t4 release semaphore
# coding: utf-8
import threading
import time
def fun(semaphore, num):
# 获得信号量,信号量减一
semaphore.acquire()
print "Thread %d is running." % num
time.sleep(3)
# 释放信号量,信号量加一
semaphore.release()
if __name__=='__main__':
# 初始化信号量,数量为2
semaphore = threading.Semaphore(2)
# 运行4个线程
for num in xrange(4):
t = threading.Thread(target=fun, args=(semaphore, num))
t.start()
运行效果如下:
Thread 0 is running.
Thread 1 is running.
Thread 2 is running.
Thread 3 is running.
可以注意到线程0和1是一起打印出消息的,而线程2和3是在3秒后打印的,可以得出每次只有2个线程获得信号量,进行打印。
7.threading.BoundedSemaphore函数
一个工厂函数,返回一个新的有界信号量对象。一个有界信号量会确保它当前的值不超过它的初始值。如果超过,则引发ValueError。在大部分情况下,信号量用于守护有限容量的资源。如果信号量被释放太多次,它是一种有bug的迹象。如果没有给出,value默认为1。
def threading.BoundedSemaphore(*args,**kargs)
def BoundedSemaphore(*args, **kwargs):
"""A factory function that returns a new bounded semaphore.
A bounded semaphore checks to make sure its current value doesn't exceed its
initial value. If it does, ValueError is raised. In most situations
semaphores are used to guard resources with limited capacity.
If the semaphore is released too many times it's a sign of a bug. If not
given, value defaults to 1.
Like regular semaphores, bounded semaphores manage a counter representing
the number of release() calls minus the number of acquire() calls, plus an
initial value. The acquire() method blocks if necessary until it can return
without making the counter negative. If not given, value defaults to 1.
"""
return _BoundedSemaphore(*args, **kwargs)
class _BoundedSemaphore(_Semaphore):
"""A bounded semaphore checks to make sure its current value doesn't exceed
its initial value. If it does, ValueError is raised. In most situations
semaphores are used to guard resources with limited capacity.
"""
def __init__(self, value=1, verbose=None):
_Semaphore.__init__(self, value, verbose)
self._initial_value = value
简单示例如下,
import threading
import time
def fun(semaphore, num):
# 获得信号量,信号量减一
semaphore.acquire()
print "Thread %d is running." % num
time.sleep(3)
# 释放信号量,信号量加一
semaphore.release()
# 再次释放信号量,信号量加一,这是超过限定的信号量数目,这时会报错ValueError: Semaphore released too many times
semaphore.release()
if __name__=='__main__':
# 初始化信号量,数量为2,最多有2个线程获得信号量,信号量不能通过释放而大于2
semaphore = threading.BoundedSemaphore(2)
# 运行4个线程
for num in xrange(4):
t = threading.Thread(target=fun, args=(semaphore, num))
t.start()
运行效果如下:
Thread 0 is running.
Thread 1 is running.
Thread 2 is running.
Thread 3 is running.
Exception in thread Thread-4:
Traceback (most recent call last):
File "C:\ProgramData\Anaconda2\lib\threading.py", line 801, in __bootstrap_inner
self.run()
File "C:\ProgramData\Anaconda2\lib\threading.py", line 754, in run
self.__target(*self.__args, **self.__kwargs)
File "C:/Users/kaicz/Desktop/DataStruc/urllib/exacookie.py", line 11, in fun
semaphore.release()
File "C:\ProgramData\Anaconda2\lib\threading.py", line 537, in release
raise ValueError("Semaphore released too many times")
ValueError: Semaphore released too many times
Exception in thread Thread-3:
Traceback (most recent call last):
File "C:\ProgramData\Anaconda2\lib\threading.py", line 801, in __bootstrap_inner
self.run()
File "C:\ProgramData\Anaconda2\lib\threading.py", line 754, in run
self.__target(*self.__args, **self.__kwargs)
File "C:/Users/kaicz/Desktop/DataStruc/urllib/exacookie.py", line 11, in fun
semaphore.release()
File "C:\ProgramData\Anaconda2\lib\threading.py", line 537, in release
raise ValueError("Semaphore released too many times")
ValueError: Semaphore released too many times
可以看出超过限定的信号量数目,这时会报错ValueError: Semaphore released too many times。
8.threading.Timer类
Timer(定时器)是Thread的派生类,用于在指定时间后调用一个方法。表示一个动作应该在一个特定的时间之后运行---也就是一个计时器。
构造方法:Timer(interval,function,args=[],kwargs={})
创建一个timer,在interval(秒)过去之后,将运行function,并将参数args和关键字参数kwargs传入。
interval:指定的时间;
function:要执行的方法;
args/kwargs:方法的参数;
实例方法:
Timer从Thread派生,没有增加实例方法。
import threading
def func():
print 'hello timer!'
timer = threading.Timer(5,func)
timer.start()
#输出,5秒后
hello timer!
线程延迟5秒后执行。
取消线程执行
timer通过调用它的start()方法启动线程。timer还可以通过调用cancel()方法(在它的动作开始之前)停止。timer在执行它的动作之前等待的时间间隔可能与用户指定的时间间隔不完全相同。
cancel(),停止timer,并取消timer动作的执行。这只在timer仍然处于等待阶段时才工作。
from threading import Timer
def fun():
print "hello,world"
if __name__ == '__main__':
t = Timer(5.0, fun)
t.start #开始执行线程,但是不会立马打印"hello,world"
t.cancel() #因为cancel取消了线程的执行,所以func()函数不会被执行
9.threading.local类
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
但是,局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)
每个函数一层一层调用,都这样传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。
如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象如何?
global_dict = {}
def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task_1()
do_task_2()
def do_task_1():
# 不传入std,而是根据当前线程查找:
std = global_dict[threading.current_thread()]
...
def do_task_2():
# 任何函数都可以查找出当前线程的std变量:
std = global_dict[threading.current_thread()]
...
这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。
有没有更简单的方式?ThreadLocal应运而生,不用查找dict,ThreadLocal帮你自动做这件事:
import threading
#创建全局ThreadLocal对象
local_school = threading.local()
def process_student():
print 'Hello, %s (in %s)' % (local_school.student, threading.current_thread().name)
def process_thread(name):
#绑定ThreadLocal的student
local_school.student = name
process_student()
t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
执行结果,
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)
全局变量local_school就是一个ThreadLocal对象。每个Thread对它,都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是现成的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但不可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
local是一个小写字母开头的类,用于管理thread-local(线程局部的)数据。对于同一个local,线程无法访问其他线程设置的属性。线程设置的属性不会被其他线程设置的同名属性替换。
可以把local看成是一个"线程-属性字典"的字典,local封装了从自身使用线程作为key检索对应的属性字典、再使用属性名作为key检索属性值的细节。
import threading
local = threading.local()
local.tname='main'
def func():
local.tname = 'notmain'
print 'in func:',local.tname
t1 = threading.Thread(target=func)
t1.start()
t1.join()
print 'in main:',local.tname
#输出
in func: notmain
in main: main
import threading
local = threading.local()
def func(name):
print 'current thread:%s' % threading.currentThread().name
local.name = name
print "%s in %s" % (local.name,threading.currentThread().name)
t1 = threading.Thread(target=func,args=('haibo',))
t2 = threading.Thread(target=func,args=('lina',))
t1.start()
t2.start()
t1.join()
t2.join()
#输出
current thread:Thread-1
haibo in Thread-1
current thread:Thread-2
lina in Thread-2
熟练掌握Thread、Lock、Condition就可以应对绝大多数需要使用线程的场合,某些情况下local也是非常有用的东西。
10.GIL
Python的线程虽然是真正的线程,但是解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,没执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL锁实际上把所有线程的执行都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL和Python线程的纠葛【ZZ】
GIL是什么?它对Python程序会产生怎样的影响?我们先来看一个问题,运行下面这段Python代码,CPU占用率是多少?
#请勿在工作中模仿,危险
def dead_loop():
while True:
pass
dead_loop()
答案是什么呢,占用100%?那是单核!还得是没有超线程的古董CPU。在双核CPU上,这个死循环只会吃掉一个核的工作负荷,也就是只占用50%CPU。在双核四线程CPU上(可以认为是逻辑四核),这个死循环会吃掉一个逻辑内核的工作负荷,也就是只占用25%CPU。
那如何能让它在双核机器上占用100%的CPU呢?答案很容易想到,用两个线程就行了,线程不正是并发分享CPU运算资源的吗?可惜,答案虽然对了,但是做起来可没那么简单。下面的程序在主线程之外又起了一个死循环的线程,
import threading
def dead_loop():
while True:
pass
# 新起一个死循环线程
t = threading.Thread(target=dead_loop)
t.start()
# 主线程也进入死循环
dead_loop()
t.join()
按道理它应该能做到占用两个核的CPU资源,可是实际运行情况却是没有什么改变,还是只占了50%CPU不到。这又是为什么呢?难道Python线程不是操作系统的原生线程?打开system monitor一探究竟,这个占了50%的Python进程确实是有两个线程在跑。那这两个死循环的线程为何不能沾满双核CPU资源呢?其实,幕后的黑手就是GIL。
在windows下可以通过pslist查看线程数量,可以看出在双核四线程处理器中,单线程死循环会消耗25%负荷。双线程死循环依然在25%左右,但是通过pslist查看,可以看出线程数为2。
在windows下如何查看CPU内核数?有两种方式,需要注意的是,这两种方式显示的都是线程数,比如双核四线程,会显示4个处理器,并不显示实际的物理内核数。
首先,看一下CPU的型号,i5-4310M,从网上可以查到是2核4线程的。
方式一:"我的电脑"——》管理——》设备管理器,里面有个处理器。
方式二:在任务管理器-性能选项卡中查看红框中有几个小窗口,CPU就是有几个核。
补充一个知识点,如何通过Python获取机器上CPU数量,可以通过如下方式,显示的是逻辑内核数量,即多线程数量。
>>> from multiprocessing import cpu_count
>>> print(cpu_count())
4
GIL的迷思:痛并快乐着【ZZ】
GIL的全称为Global Interpreter Lock,意即全局解释器锁。在Python语言的主流实现CPython中,GIL是一个货真价实的全局线程锁,在解释器解释执行任何Python代码时,都需要先获得这把锁才行,在遇到I/O操作时会释放这把锁。如果是纯计算的程序,没有I/O操作,解释器会每隔100次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过sys.setcheckinterval来调整)。所以,虽然CPython的线程库直接封装操作系统的原生线程,但CPython进程做为一个整体,同一时间只会有一个获得了GIL的线程在跑,其它的线程都出于等待状态,等着GIL的释放。这也就解释了我们上面的实验结果:虽然有两个死循环的线程,而且有两个物理CPU内核,但因为GIL的限制,两个线程只是做着分时切换,总的CPU占用率还略低于50%。
看起来Python很不给力啊。GIL直接导致CPython不能利用物理多核的性能加速运算。那为什么会有这样的设计呢?我猜想应该还是历史遗留问题。多核 CPU 在 1990 年代还属于类科幻,Guido van Rossum 在创造 python 的时候,也想不到他的语言有一天会被用到很可能 1000+ 个核的 CPU 上面,一个全局锁搞定多线程安全在那个时代应该是最简单经济的设计了。简单而又能满足需求,那就是合适的设计(对设计来说,应该只有合适与否,而没有好与不好)。怪只怪硬件的发展实在太快了,摩尔定律给软件业的红利这么快就要到头了。短短 20 年不到,代码工人就不能指望仅仅靠升级 CPU 就能让老软件跑的更快了。在多核时代,编程的免费午餐没有了。如果程序不能用并发挤干每个核的运算性能,那就意谓着会被淘汰。对软件如此,对语言也是一样。那 Python 的对策呢?
Python 的应对很简单,以不变应万变。在最新的 python 3 中依然有 GIL。之所以不去掉,原因嘛,不外以下几点:
欲练神功,挥刀自宫:
CPython的GIL本意是用来保护所有全局的解释器和环境状态变量的。如果去掉GIL,就需要多个更细粒度的锁对解释器的众多全局状态进行保护。或者采用 Lock-Free 算法。无论哪一种,要做到多线程安全都会比单使用GIL一个锁要难的多。而且改动的对象还是有20年历史的CPython代码树,更不论有这么多第三方的扩展也在依赖GIL。对Python社区来说,这不异于挥刀自宫,重新来过。
就算自宫,也未必成功:
有位牛人曾经做了一个验证用的 CPython,将GIL去掉,加入了更多的细粒度锁。但是经过实际的测试,对单线程程序来说,这个版本有很大的性能下降,只有在利用的物理 CPU 超过一定数目后,才会比 GIL 版本的性能好。这也难怪。单线程本来就不需要什么锁。单就锁管理本身来说,锁GIL 这个粗粒度的锁肯定比管理众多细粒度的锁要快的多。而现在绝大部分的Python程序都是单线程的。再者,从需求来说,使用Python绝不是因为看中它的运算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。费了大力气把 GIL 拿掉,反而让大部分的程序都变慢了,这不是南辕北辙吗。
难道Python这么优秀的语言真的仅仅因为改动困难和意义不大就放弃多核时代了吗?其实,不做改动最最重要的原因还在于:不用自宫,也一样能成功!
其它神功
那除了切掉GIL外,还有方法让Python在多核时代活的滋润?让我们回到本文最初的那个问题:如何能让这个死循环的Python脚本在双核机器上占用100%的CPU?其实最简单的答案应该是:运行两个Python死循环的程序!也就是说,用两个分别沾满一个CPU内核的Python进程来做到。
确实,多进程也是利用多个CPU的好方法。只是进程间内存地址空间独立,互相协同通信要比多线程麻烦很多。有感于此,Python在2.6里新引入了multiprocessing这个多进程标准库,让多进程的Python程序编写简化到类似多线程的程度,大大减轻了GIL带来的不能利用多核的尴尬。
这还只是一个方法,如果不想用多进程这样重量级的解决方案,还有个更彻底的方案,放弃Python,改用C/C++。当然,你也不用做的这么绝,只需要把关键部分用C/C++写成Python扩展,其它部分还是用Python来写,让Python的归Python,C的归C。
一般计算密集性的程序都会用C代码编写并通过扩展的方式集成到Python脚本里(如NumPy模块)。在扩展里就完全可以用C创建原生线程,而且不用锁GIL,充分利用CPU的计算资源了。不过,写Python扩展总是让人觉得很复杂。好在Python还有另一种与C模块进行互通的机制:ctypes。
利用ctypes绕过GIL
ctypes与Python扩展不同,它可以让Python直接调用任意的C动态库的导出函数。你所要做的只是用ctypes写些Python代码即可。最酷的是,ctypes会在调用C函数前释放GIL。所以,我们可以通过ctypes和C动态库来让python充分利用物理内核的计算能力。让我们来实际验证一下,这次我们用C写一个死循环函数。
extern"C"
{
void DeadLoop()
{
while (true);
}
}
用上面的C代码编译生成动态库libdead_loop.so(Windows上是dead_loop.dll),接着就要利用ctypes来在python里load这个动态库,分别在主线程和新建线程里调用其中的DeadLoop。
from ctypes import *
from threading import Thread
lib = cdll.LoadLibrary("libdead_loop.so")
t = Thread(target=lib.DeadLoop)
t.start()
lib.DeadLoop()
这回再看看system monitor,Python解释器进程有两个线程在跑,而且双核CPU全被占满了,ctypes确实很给力!需要提醒的是,GIL是被ctypes在调用C函数前释放的。但是Python解释器还是会在执行任意一段 Python代码时锁GIL的。如果你使用Python的代码做为C函数的callback,那么只要Python的callback方法被执行时,GIL还是会跳出来的。比如下面的例子:
extern"C"
{
typedef void Callback();
void Call(Callback* callback)
{
callback();
}
}
from ctypes import *
from threading import Thread
def dead_loop():
while True:
pass
lib = cdll.LoadLibrary("libcall.so")
Callback = CFUNCTYPE(None)
callback = Callback(dead_loop)
t = Thread(target=lib.Call, args=(callback,))
t.start()
lib.Call(callback)
注意这里与上个例子的不同之处,这次的死循环是发生在Python代码里(DeadLoop函数)而C代码只是负责去调用这个callback而已。运行这个例子,你会发现CPU占用率还是只有50%不到。GIL又起作用了。
其实,从上面的例子,我们还能看出ctypes的一个应用,那就是用Python写自动化测试用例,通过ctypes直接调用C模块的接口来对这个模块进行黑盒测试,哪怕是有关该模块C接口的多线程安全方面的测试,ctypes也一样能做到。
虽然CPython的线程库封装了操作系统的原生线程,但却因为GIL的存在导致多线程不能利用多个CPU内核的计算能力。好在现在Python有了易筋经(multiprocessing), 吸星大法(C语言扩展机制)和独孤九剑(ctypes),足以应付多核时代的挑战,GIL切还是不切已经不重要了。
queue队列
适用于多线程编程的先进先出数据结构,可以用来安全的传递多线程信息。
queue.Queue类
构造方法:Queue([maxsize])
构造一个先进先出的队列。
maxsize:指定队列长度,为0时,表示队列长度无限制;
实例方法:
q.join():等到队列为空的时候,才去执行别的操作。否则,就一直阻塞。不是说通过get取完队列之后,就不阻塞了,而是要每次get之后要执行task_done(),这样,等队列为空时,join才不阻塞。
下面的程序是阻塞的,
>>> import Queue
>>> q = Queue.Queue(5)
>>> q.put(123)
>>> q.put(456)
>>> q.get()
123
>>> q.get()
456
>>> q.join() #阻塞
下面的程序是不阻塞的,
>>> import Queue
>>> q = Queue.Queue(5)
>>> q.put(123)
>>> q.put(456)
>>> q.get()
123
>>> q.task_done()#在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
>>> q.get()
456
>>> q.task_done()
>>> q.join()
>>>
q.qsize():返回队列的大小(不可靠);
q.empty():当队列为空的时候,返回True。否则,返回False。
q.full():当队列满的时候,返回True。否则,返回False。
q.put(item, block=True, timeout=None) :将item放入Queue尾部,item必须传入。可选参数block默认为True,表示当队列满时,会等待队列给出可用位置,为False时为非阻塞,此时如果队列已满,会引发queue.Full异常。可选参数timeout表示阻塞的时间,如果过期后,队列仍无法给出放入item的位置,则引发queue.Full异常。
q.get(block=True, timeout=None):移除并返回队列头部的一个值。可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞,为False时,不阻塞,若此时队列为空,则引发queue.Empty异常。可选参数timeout,表示阻塞的时间,如果过期后,如果队列为空,则引发queue.Empty异常。
q.put_nowait(item):等效于put(item,block=False)。
q.get_nowait():等效于get(item,block=False)。
>>> import Queue
>>> q = Queue.Queue(2)#如果没有参数的话,就可以放无限多的数据。
>>> print q.empty()#返回队列是否为空。空则为True,此处为True。
True
>>> q.put(11)
>>> q.put(22)
>>> print q.empty()#此处为False。
False
>>> print q.qsize()#返回队列中现在有多少元素。
2
>>> q.put(33)#阻塞。如果队列最大能放2个元素,这时候放了第三个,默认是阻塞的。
>>> q.put(33,block=False)#如果block=False,则会报错Queue.Full。
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
q.put(33,block=False)
Full
>>> q.put(33,block=True,timeout=2)#设置为阻塞,如果队列满了,在timeout设置的时间之内,队列中现有元素没有被取过,则会报错Queue.Full。
Traceback (most recent call last):
File "<pyshell#17>", line 1, in <module>
q.put(33,block=True,timeout=2)
Full
>>> print q.get()
11
>>> print q.get()
22
>>> print q.get(timeout=2)# 队列里的数据已经取完了,如果再继续取,就会阻塞。这里timeout时间2秒,就是等待2秒,队列里还没有数据就报错:Queue.Empty
Traceback (most recent call last):
File "<pyshell#20>", line 1, in <module>
print q.get(timeout=2)
Empty
其他队列
Queue.Queue:先进先出队列
Queue.LifoQueue:后进先出队列
Queue.PriorityQueue:优先级队列
Queue.deque:双向队列
生产者消费者模型(队列)
举一个寄信的例子,假设你要寄一封信,大致过程如下:
1.你把信写好---相当于生产者生产数据;
2.你把信放入信箱---相当于生产者把数据放入缓冲区;
3.邮递员把信从信箱取出,做相应处理---相当于消费者把数据从缓冲区取出,处理数据;
生产者消费者模型的作用
1.解耦,修改生产者,不会影响消费者,反之亦然。
假设生产者和消费者分别是两个线程,如果让生产者直接调用消费者的某个方法,那么生产者就会对消费者产生依赖(也就是耦合)。如果未来消费者的代码发生变化,可能会影响到生产者的代码。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了。
举个例子,我们去邮局投递信件,如果不使用信箱(也就是缓冲区),你必须把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他。这就产生了你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致生产者修改代码)。而邮箱相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
2.并发,解决阻塞。
由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区通信的,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
继续上面的例子,如果我们不使用信箱,就得在邮局等待邮递员,直到他回来,把信件交给他,这期间我们啥事儿都不能干(也就是生产者阻塞)。或者,邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。
3.支持忙闲不均
当生产者制造数据快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中,慢慢处理掉。而不至于因为消费者的性能造成数据丢失或影响生产者生产。
再拿寄信的例子,假设邮递员一次只能带走1000封信,万一碰上情人节(或是圣诞节)送贺卡,需要寄出去的信超过了1000封,这时候邮箱这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在信箱中,等下次过来时再拿走。
在生产环境,用生产者消费者模型,就可以解决:
1.处理瞬时并发的请求问题。瞬时的连接数就不会沾满,所以服务器就不会挂了;
2.客户端提交一个请求,不用等待处理完毕,可以在页面上做别的事情;
如果不用队列存数据,服务器通过多线程来处理数据:
用户往队列存数据,服务器从队列里取数据。
没有队列的话,就跟最大连接数有关系,每个服务器有最大连接数。
客户端要获取服务器返回,服务器要查、修改数据库或修改文件,要2分钟,那客户端就要挂起2分钟,2万个连接一半都要挂起,服务器就崩溃了。
如果没有队列,第一个用户发来请求,连上服务器,占用连接,等待2分钟。第二个人来也要占用2分钟。
web服务器如果要处理并发,有10万并发。如果一台机器接收一个连接,需要10万个机器,等待2分钟就处理完了。
把请求放在队列的好处
用户发来请求,把请求放到队列里,可以让连接马上断开,不会阻塞,就不占用服务器的连接数了。如果要看订单处理了没,就要打开另外一个页面,查看请求是否处理。
服务器查询处理任务的时候,每个仍需要花2分钟,服务器总耗时是没有减少的。但是这样做,客户端就不会持续的占用连接了。那瞬时的连接数就不会占满,所以服务器就不会挂了。
但是,后台要处理10万个请求,也需要50台服务器,并不会减少服务器数量。这样就能处理瞬时并发的请求问题。
服务器只是处理请求,修改数据库的值,不是告诉客户端。而是,客户端再发来请求,查询数据库已经修改的内容。
提交订单之后,把这个订单扔给队列,程序返回"正在处理",就不等待了,然后断开这个连接,你可以在页面里做别的事情,不用一直等待订单处理完。这样就不影响服务器的最大连接数。在页面帮你发起一个ajax请求,不停的请求(可能是定时器),"我的订单成功没有,我的订单成功没有,...",如果订单成功了,就自动返回页面,订单成功。
如果不用队列的话,一个请求就占用一个服务器,等待的人特别多,等待连接的个数太多了,服务器就挂掉了。队列就没有最大个数限制,把请求发给队列了,然后http连接就断开了,就不用等待了。
12306买票的时候,下次再来请求的时候,就会告诉你,前面排了几个人。
Python queue的特点
Python的Queue是内存级别的,RabbitMQ可以把队列发到别的服务器上处理。
所以,Python里的Queue不能持久化,但是,RabbitMQ可以持久化。
生产者消费者代码示例:
import time,random
import Queue,threading
q = Queue.Queue()
def Producer(name):
count = 1
while True:
time.sleep(random.randrange(3))
if q.qsize() < 3:
q.put(count)
print "Producer %s has produced %s baozi..." %(name, count)
count += 1
def Consumer(name):
count = 1
while True:
time.sleep(random.randrange(4))
if not q.empty():
data = q.get()
print data
print '\033[32;1mConsumer %s has eat %s baozi...\033[0m' % (name,data)
else:
print "---no baozi anymore---"
count += 1
A = threading.Thread(target=Producer, args=('A',))
B = threading.Thread(target=Consumer, args=('B',))
C = threading.Thread(target=Consumer, args=('C',))
A.start()
B.start()
C.start()
执行结果如下:
Producer A has produced 1 baozi...1
Consumer B has eat 1 baozi...
Producer A has produced 2 baozi...
2
Consumer B has eat 2 baozi...
---no baozi anymore---
Producer A has produced 3 baozi...
Producer A has produced 4 baozi...
Producer A has produced 5 baozi...
3
Consumer B has eat 3 baozi...
4
Consumer B has eat 4 baozi...
5
Consumer C has eat 5 baozi...
---no baozi anymore---
---no baozi anymore---
---no baozi anymore---
---no baozi anymore---
Producer A has produced 6 baozi...
6
Consumer C has eat 6 baozi...
---no baozi anymore---
Producer A has produced 7 baozi...
7
Consumer C has eat 7 baozi...
...
再看一个示例代码:
from Queue import Queue
import random, threading, time
#生产者类
class Producer(threading.Thread):
def __init__(self, name, queue):
super(Producer, self).__init__(name=name)
self.data = queue
def run(self):
for i in range(5):
print "%s is producing %d to the queue!" % (self.getName(), i)
self.data.put(i)
time.sleep(random.randrange(10)/5)
print "%s finished!" %self.getName()
#消费者类
class Consumer(threading.Thread):
def __init__(self, name, queue):
super(Consumer,self).__init__(name=name)
self.data = queue
def run(self):
for i in range(5):
val = self.data.get()
print "%s is consuming. %d in the queue is consumed!" % (self.getName(),val)
time.sleep(random.randrange(10))
print "%s finished!" % self.getName()
def main():
queue = Queue()
producer = Producer('Producer', queue)
consumer = Consumer('Consumer', queue)
producer.start()
consumer.start()
producer.join()
consumer.join()
print "All threads finished!"
if __name__ == '__main__':
main()
执行结果可能如下,注意多线程是抢占式执行的,所以,打印出的运行结果不一定和下面的完全一样。
Producer is producing 0 to the queue!
Producer is producing 1 to the queue!
Consumer is consuming. 0 in the queue is consumed!
Producer is producing 2 to the queue!
Producer is producing 3 to the queue!
Producer is producing 4 to the queue!
Producer finished!
Consumer is consuming. 1 in the queue is consumed!
Consumer is consuming. 2 in the queue is consumed!
Consumer is consuming. 3 in the queue is consumed!
Consumer is consuming. 4 in the queue is consumed!
Consumer finished!
All threads finished!
进程
由于GIL的存在,它会将进程中的线程序列化,也就是多核CPU实际上并不能达到并行提高速度的目的,如果想要充分地使用多核CPU的资源,在Python中需要使用多进程,使用多进程是不受限的。
如果每个子进程执行需要消耗的时间非常短(执行+1操作等),这不必使用多进程,因为进程的启动关闭也会耗费资源。
当然,使用多进程往往是用来处理CPU密集型(科学计算)的需求,如果是IO密集型(文件读取,爬虫等)则可以使用多线程去处理。
Python提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python就会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识。Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:
# multiprocessing.py
import os
print 'Process (%s) start...' % os.getpid()
pid = os.fork()
if pid==0:
print 'I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid())
else:
print 'I (%s) just created a child process (%s).' % (os.getpid(), pid)
运行结果如下:
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
由于Windows没有fork调用,上面的代码在Windows上无法运行。由于Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的。
有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。
multiprocessing
如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。它既可以用来编写多进程,也可以用来编写多线程。如果是多线程的话,用multiprocessing.dummy即可。
multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
from multiprocessing import Process
import os
# 子进程要执行的代码
def run_proc(name):
print 'Run child process %s (%s)...' % (name, os.getpid())
if __name__=='__main__':
print 'Parent process %s.' % os.getpid()
p = Process(target=run_proc, args=('test',))
print 'Process will start.'
p.start()
p.join()
print 'Process end.'
执行结果如下:
Parent process 928.
Process will start.
Run child process test (929)...
Process end.
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
multiprocessing常用组件及功能
创建进程模块:
Process:用于创建进程的类
Pool:用于创建进程池的类
Queue:用于进程通信,资源共享
Value,Array:用于进程通信,资源共享
Pipe:用于管道通信
Manager:用于资源共享
同步子进程模块:
Condition
Event
Lock
RLock
Semaphore
multiprocessing.Process类
利用multiprocessing.Process可以创建一个进程实例,该Process对象与Thread对象用法相同,也有start(),run,join()等方法。Process类适合创建简单的进程,如需资源共享可以结合multiprocessing.Queue使用,如果想要控制进程数量,则建议使用进程池Pool类。
构造方法:Process([group [,target [,name [,args [,kwargs]]]]])
group:总是None;
target:要执行的方法,会被run()方法激活的调用对象;
name:进程名;
args:调用对象的参数元组;
kwargs:调用对象的关键字参数字典;
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度。
run():strat()调用run方法,如果实例进程未指定传入target时,这时start执行默认run()方法。
terminate():不管任务是否完成,立即停止工作进程。
属性:
authkey
daemon:和线程的setDeamon功能一样
exitcode(进程在运行时为None。如果为–N,表示被信号N结束)
name:进程名字。
pid:进程号。
创建多进程的两种方法
Process类中,可以使用两种方法创建子进程。
使用Process创建子进程
说明:用法与Threading相似。
from multiprocessing import Process #导入Process模块
import os
def test(name):
"""
函数输出当前进程ID,以及其父进程ID。
此代码应在Linux下运行,因为windows下os模块不支持getppid()
"""
print "Process ID: %s" %(os.getpid())
#print "Parent Process ID: %s" %(os.getppid())
if __name__ == "__main__":
"""
windows下,创建进程的代码一下要放在main函数里面
"""
proc = Process(target=test, args=('nmask',))
proc.start()
proc.join()
使用Process类继承,创建子进程
说明:通过继承Process类,修改run函数代码。
from multiprocessing import Process #导入Process模块
import time
class MyProcess(Process):
'''
继承Process类,类似threading.Thread
'''
def __init__(self, arg):
super(MyProcess, self).__init__()
self.arg = arg
def run(self):
'''
重构run函数
'''
print 'nMask', self.arg
time.sleep(1)
if __name__ == '__main__':
for i in range(10):
p = MyProcess(i)
p.start()
# for i in range(10):
# p.join()
执行结果如下,
nMask 1
nMask 0
nMask 3
nMask 4
nMask 2
nMask 5
nMask 6
nMask 7
nMask 8
nMask 9
Multiprocessing.Pool类(进程池)
Multiprocessing.Pool可以提供指定数量的进程供用户调用。进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程。如果进程池中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。也就是,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求。但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行它。
构造方法:Pool([processes[,initializer[,initargs[,maxtasksperchild]]]])
Pool类用于需要执行的目标很多的情况,而手动限制进程数量又太繁琐。如果目标少且不用控制进程数量,则可以用Process类。
processes:使用的工作进程的数量。如果processes是None,那么,使用multiprocessing.cpu_count()返回的数量;
initializer:如果initializer是None,那么每一个工作进程在开始的时候,会调用initializer(*initargs);
maxtasksperchild:工作进程退出之前可以完成的任务数,完成后用一个新的工作进程来替代原进程,让闲置的资源被释放。maxtasksperchild默认是None,意味着只要Pool存在,工作进程就会一直存活;
注意:Pool对象的方法只可以被创建的进程所调用。
实例方法:
apply(func[,args[,kwargs]]):使用arg和kwds参数调用func函数,结果返回前会一直阻塞,由于这个原因,apply_async()更适合并发执行,另外,func函数仅被pool中的一个进程运行。
apply_async(func[,args[,kwargs[,callback[,error_callback]]]]): apply()方法的一个变体,会返回一个结果对象。如果callback被指定,那么callback可以接收一个参数然后被调用,当结果准备好回调时会调用callback,调用失败时,则用error_callback替换callback。Callbacks应被立即完成,否则处理结果的线程会被阻塞。
close():关闭pool,阻止更多的任务提交到pool,使其不再接受新的任务。待任务完成后,工作进程会退出。
terminate():不管任务是否完成,立即停止工作进程。在对pool对象进程垃圾回收的时候,会立即调用terminate()。
join():主进程阻塞,等待子进程的退出。wait工作线程的退出,在调用join()前,必须调用close()orterminate()。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。
map(func, iterable[, chunksize])
map_async(func, iterable[, chunksize[, callback[, error_callback]]])
imap(func, iterable[, chunksize])
imap_unordered(func, iterable[, chunksize])
starmap(func, iterable[, chunksize])
starmap_async(func, iterable[, chunksize[, callback[, error_back]]])
示例一,Pool+map函数
说明,此写法缺点在于只能通过map像函数传递一个参数。
from multiprocessing import Pool
def f(x):
print x
if __name__=='__main__':
lists = [1,2,3]
pool=Pool(2) #定义最大的进程数
pool.map(f, lists)
pool.close()
pool.join()
执行结果如下,
1
2
3
from multiprocessing import Pool
def f(x):
return x**2
if __name__ == '__main__':
pool = Pool(3)
rel = pool.map(f,[1,2,3,4,5,6,7,8,9,10])
print(rel)
pool.close()
pool.join()
执行结果如下,
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
异步进程池(非阻塞)
from multiprocessing import Pool
def test(i):
return i
if __name__ == '__main__':
pool = Pool(10)
for i in xrange(500):
'''
For循环中执行步骤:
1.循环遍历,将500个子进程添加到进程池(相对父进程会阻塞)
2.每次执行10个子进程,等一个子进程执行完后,立马启动新的子进程。(相对父进程不阻塞)
apply_async为异步进程池写法。
异步指的是启动子进程的过程,与父进程本身的执行(print)是异步的,而For循环中往进程池添加子进程的过程,与父进程本身的执行却是同步的。
'''
pool.apply_async(test,args=(i,))#维持执行的进程总数为10,当一个进程执行完后启动一个新进程.
print "test"
pool.close()
pool.join()
执行结果如下,可以发现,主进程的打印语句先输出了,因为异步。进程池中的打印输出,每次打印10个数,然后因为time.sleep(3)停顿,然后,输出下一批打印10个数,以此类推。
test
0
1
23
4
5
6
7
8
...
执行顺序:for循环内执行了2个步骤,第一步:将500个对象放入进程池(阻塞)。第二步:同时执行10个子进程(非阻塞),有结束的就立即添加,维持10个子进程运行。
apply_async方法会在执行完for循环的添加步骤(第一步???)后,直接执行后面的print语。而apply方法会等所有进程池中的子进程运行完以后再执行后面的print语句。
注意:调用join之前,先调用close或者terminate方法,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束。
同步进程池(阻塞)
from multiprocessing import Pool
import time
def test(i):
time.sleep(3)
print i
if __name__=="__main__":
pool = Pool(processes=10)
for i in xrange(500):
'''
实际测试发现,for循环内部执行步骤:
1.遍历500个可迭代对象,往进程池放一个子进程
2.执行这个子进程,等子进程执行完毕,再往进程池放一个子进程,再执行。(同时只执行一个子进程)
for循环执行完毕,再执行print函数。
'''
pool.apply(test, args=(i,)) #维持执行的进程总数为10,当一个进程执行完后启动一个新进程.
print "test"
pool.close()
pool.join()
执行结果如下,所有进程池中的进程执行完毕后,才执行的主进程。
说明:for循环内执行的步骤顺序,往进程池中添加一个子进程,执行子进程,等待执行完毕再添加一个子进程…,等500个子进程都执行完了,再执行print "test"。(从结果来看,并没有多进程并发)
1
2
...
497
498
499
test
进程间通讯
不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以用以下方法。multiprocessing包中有Pipe类和Queue类来分别支持这两种IPC机制
进程队列Queue
Queue,先进先出的结构。Queue允许多个进程放入,多个进程从队列取出对象。使用multiprocessing.Queue(maxsize)创建,maxsize表示队列中可以存放对象的最大数量。
构造方法:Queue([maxsize])
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
实例方法:
q.cancel_join_thread():不会在进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。
q.close():关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。
q.empty():如果调用此方法时q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。
q.full():如果q已满,返回为True。由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。
q.get([block[,timeout]]):返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在指定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。
q.get_nowait():同q.get(False)方法。
q.join_thread():连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。
q.put(item[,block[,timeout]]):将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。
q.qsize():返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。
JoinableQueue([maxsize]):创建可连接的共享进程队列。这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
JoinableQueue的实例p除了与Queue对象相同的方法之外,还具有以下方法:
q.task_done():使用者使用此方法发出信号,表示q.get()返回的项目已经被处理。如果调用此方法的次数大于从队列中删除的项目数量,将引发ValueError异常。
q.join():生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。
下面的程序展示了Queue的使用,
from multiprocessing import Process, Queue
def f(q):
q.put([42, None, 'hello'])
if __name__ == '__main__':
q = Queue() #创建一个Queue对象
p = Process(target=f, args=(q,)) #创建一个进程
p.start()
print q.get()
p.join()
#输出
[42, None, 'hello']
上面是一个queue的简单应用,使用队列q对象调用get函数来取得队列中最先进入的数据。
下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。
from multiprocessing import Process, JoinableQueue
def consumer(input_q):
while True:
item=input_q.get()
#处理项目
print item
#发出信号,通知任务完成
input_q.task_done()
def producer(sequence, output_q):
for item in sequence:
#将项目放入队列
output_q.put(item)
#建立进程
if __name__ == '__main__':
q = JoinableQueue()
#运行使用者进程
cons_p = Process(target=consumer,args=(q,))
cons_p.daemon = True #定义该进程为后台运行
cons_p.start()
#生产项目,sequence代表要发送给使用者的项目序列
#在实践中,这可能是生成器的输出或通过一些其它方式生产出来
sequence = range(5)
producer(sequence, q)
#等待所有项目被处理
q.join()
#输出
0
1
2
3
4
如果需要,可以在同一个队列中放置多个进程,也可以从同一个队列中获取多个进程。例如,如果要构造使用者进程池,可以编写下面这样的代码:
from multiprocessing import Process, JoinableQueue
def consumer(input_q):
while True:
item=input_q.get()
#处理项目
print item
#发出信号,通知任务完成
input_q.task_done()
def producer(sequence, output_q):
for item in sequence:
#将项目放入队列
output_q.put(item)
#建立进程
if __name__ == '__main__':
q = JoinableQueue()
#创建一些使用者进程
cons_p1 = Process(target=consumer,args=(q,))
cons_p1.daemon = True #定义该进程为后台运行
cons_p1.start()
cons_p2 = Process(target=consumer,args=(q,))
cons_p2.daemon = True #定义该进程为后台运行
cons_p2.start()
#生产项目,sequence代表要发送给使用者的项目序列
#在实践中,这可能是生成器的输出或通过一些其它方式生产出来
sequence = range(10)
producer(sequence, q)
#等待所有项目被处理
q.join()
#输出
0
1
2
3
4
在某些应用程序中,生产者需要通知使用者,他们不再生产任何项目而且应该关闭。为此,编写的代码中应该使用标志(sentinel)-指示完成的特殊值。下面这个例子使用None作为标志说明这个概念:
from multiprocessing import Process, JoinableQueue
def consumer(input_q):
while True:
item=input_q.get()
if item is None:
break
#处理项目
print item
#关闭
print "Consumer Done"
def producer(sequence, output_q):
for item in sequence:
#将项目放入队列
output_q.put(item)
#建立进程
if __name__ == '__main__':
q = JoinableQueue()
#创建一些使用者进程
cons_p = Process(target=consumer,args=(q,))
cons_p.start()
#生产项目,sequence代表要发送给使用者的项目序列
#在实践中,这可能是生成器的输出或通过一些其它方式生产出来
sequence = range(5)
producer(sequence, q)
#在队列上安置标志,发出完成信号
q.put(None)
#等待使用者进程关闭
cons_p.join()
#输出
0
1
2
3
4
Consumer Done
如果像上面这个例子中那样使用标志,一定要在队列上为每个使用者上都安置标志。例如,如果有三个使用者进程使用队列上的项目,那么生产者需要在队列上安置三个标志,才能让所有使用者都关闭。
管道Pipe
Pipe可以是单向(half-duplex),也可以是双向(duplex)。我们通过mutiprocessing.Pipe(duplex=False)创建单向管道 (默认为双向)。一个进程从PIPE一端输入对象,然后被PIPE另一端的进程接收,单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。
示例一(单向),
from multiprocessing import Process, Pipe
def f(conn): #conn为父线程传递过来的pipe对象
conn.send([42,None,'hello']) #在pipe对象的一端发送数据
conn.close() #关闭
if __name__ == '__main__':
parent_conn, child_conn = Pipe() #创建两个pipe对象
p = Process(target=f, args=(child_conn,)) #创建一个进程,传递的参数为pipe对象
p.start()
print parent_conn.recv() #在父进程中使用另一个pipe对象的recv()方法接收数据
p.join()
#输出
[42, None, 'hello']
每个连接对象都有send和recv方法。需要注意的是,如果两个进程或者线程同时读取或写入pipe对象的终端,则可能引起异常。如果同时使用pipe的不同终端,则不会有风险。
示例二(双向),
from multiprocessing import Process, Pipe, freeze_support
import time
def proc1(pipe):
pipe.send('hello')
time.sleep(1)
print 'proc1 rec:', pipe.recv()
def proc2(pipe):
print 'proc2 rec:', pipe.recv()
pipe.send('hello, too')
if __name__ == '__main__':
freeze_support()
pipe1, pipe2 = Pipe() #创建两个pipe对象
#Pass an end of the pipe to process 1
p1 = Process(target=proc1, args=(pipe1,)) #创建p1进程
#Pass the other end of the pipe to process 2
p2 = Process(target=proc2, args=(pipe2,)) #创建p2进程
p1.start() #启动进程p1
p2.start() #启动进程p2
p1.join() #这里等待进程1执行完成
p2.join() #等待进程2执行完成
#输出
proc2 rec: hello
proc1 rec: hello, too
这个例子实现了两个进程之间的通信,实现了双向通信。进程proc1中发送"hello"字符串,进程proc2中接收进程proc1中发送的"hello"字符串并输出。然后proc2发送"hello,too"字符串,进程proc1接收到proc2发送的字符串并输出。
Manager
Python实现多进程间通信的方式有很多种,例如队列,管道等。但是,这些方式只适用于多个进程都是源于同一个父进程的情况。如果多个进程不是源于同一个父进程,只能用共享内存、信号量等方式,但是这些方式对于复杂的数据结构,使用起来比较麻烦,不够灵活。
Manager是一种较为高级的多进程通信方式,它能支持Python支持的任何数据结构。它的原理是:先启动一个ManagerServer进程,这个进程是阻塞的,它监听一个socket,然后其他进程(ManagerClient)通过socket来连接到ManagerServer,实现通信。
由Manager()返回的manager提供list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Queue,Value and Array类型的支持。
from multiprocessing import Process,Manager
import os
def f(d, l):
d[os.getpid()] = os.getpid()
d[1] = '1'
l.append(os.getpid())
print 'In f:',l
if __name__ == '__main__':
with Manager() as manager:
d = manager.dict()#生成一个可在多个进程间共享和传递的字典
l = manager.list(range(5))#生成一个可在多个进程间共享和传递的列表
p_list = []
for i in range(5):
p = Process(target=f, args=(d,l))
p.start()
p_list.append(p)
for res in p_list:
res.join()
print 'd is',d
print 'l is',l
#输出
In f: [0, 1, 2, 3, 4, 604]
In f: [0, 1, 2, 3, 4, 604, 9492]
In f: [0, 1, 2, 3, 4, 604, 9492, 13104]
In f: [0, 1, 2, 3, 4, 604, 9492, 13104, 13372]
In f: [0, 1, 2, 3, 4, 604, 9492, 13104, 13372, 11976]
d is {1: '1', 11976: 11976, 13372: 13372, 13104: 13104, 9492: 9492, 604: 604}
l is [0, 1, 2, 3, 4, 604, 9492, 13104, 13372, 11976]
注意:windows操作系统下,创建multiprocessing类对象代码一定要放在__name__ == '__main__'下。而linux不需要。
Manager比shared memory更灵活,因为它可以支持任意的对象类型。另外,一个单独的Manager可以通过进程在网络上不同的计算机之间共享,不过它比shared memory要慢。
共享内存Shared Memory
基本特点:
1.共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
2.为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。(文件映射)。
3.由于多个进程共享一段内存,因此,也需要依靠某种同步机制。
优缺点:
优点:快速在进程间传递数据;
缺点:数据安全上存在风险,内存中的内容会被其他进程覆盖或者篡改;
共享内存要符合C语言的使用语法,from multiprocessing import Value,Array。
Value:将一个值存放在内存中;
Array:将多个数据存放在内存中,但是,要求数据类型一致;
def Value(typecode_or_type, *args, **kwds)
功能:得到一个共享内存对象,并且存入初始值。Returns a synchronized shared object(同步共享对象)。
typecode_or_type:定义了返回类型(转换成C语言中存储类型),它要么是一个ctypes类型,要么是一个代表ctypes类型的code。
*args:开辟一个空间,并赋一个args值,值的类型不限。
注:ctypes是Python的一个外部函数库,它提供了和C语言兼容的数据类型,可以调用DLLs或者共享库的函数。
from multiprocessing import Process,Value
import time
import random
def save_money(money):
for i in range(100):
time.sleep(0.1)
money.value += random.randint(1,200)
def take_money(money):
for i in range(100):
time.sleep(0.1)
money.value -= random.randint(1,150)
if __name__ == '__main__':
#money为共享内存对象,给他一个初始值2000,类型为整形"i"
#相当于开辟了一个空间,同时绑定值2000
money = Value('i',2000)
d = Process(target=save_money,args=(money,)) #这里面money是全局的,不写也可以
d.start()
w = Process(target=take_money,args=(money,)) #这里面money是全局的,不写也可以
w.start()
d.join()
w.join()
print money.value
#输出
4960
def Array(typecode_or_type, size_or_initializer, **kwds)
使用基本类似于Value,Returns a synchronized shared array。
typecode_or_type:定义了返回类型(转换成C语言中存储类型),它要么是一个ctypes类型,要么是一个代表ctypes类型的code。
size_or_initializer:初始化共享内存空间。若为数字,表示开辟的共享内存空间的大小。若为数组,表示在共享内存中存入数组。
from multiprocessing import Process,Array
def fun(m,n):
for i in range(n):
print m[i]
if __name__ == '__main__':
#表示开辟3个空间,且均为整形i,其实就是一个列表
m = Array('i',3)
p = Process(target= fun, args=(m,3))
p.start()
p.join()
#输出
0
0
0
说明:三个0表示开辟的共享内存容量为3,当再超过3时就会报错"IndexError: invalid index"。
from multiprocessing import Process,Array
import time
def fun(m,n):
for i in range(n):
m[i] = i
if __name__ == '__main__':
#表示开辟5个空间,且均为整形i,其实就是一个列表
m = Array('i',5)
p = Process(target= fun, args=(m,5))
p.start()
time.sleep(1)
for i in m:
print i
p.join()
#输出
0
1
2
3
4
如果将time.sleep(1)去掉,则输出结果均为0,原因就是还未赋值就已经打印了。
from multiprocessing import Process,Array
import time
def fun(m,n):
for i in range(n):
print m[i]
m[i] = i
if __name__ == '__main__':
#表示开辟5个空间,且均为整形i,其实就是一个列表
m = Array('i',[1,2,3,4,5])
p = Process(target= fun, args=(m,5))
p.start()
time.sleep(1)
for i in m:
print i
p.join()
#输出
1
2
3
4
5
0
1
2
3
4
第二个参数如果传入一个数字,则表示在共享内存中开辟多大的空间。
如果传入的是列表,则开辟相应元素数量的共享空间容量,并将其直接存入共享空间。
from multiprocessing import Process,Value,Array
def f(n,a):
n.value = 3.1415727
for i in range(len(a)):
a[i] = -a[i]
if __name__ == '__main__':
num = Value('d', 0.0)
arr = Array('i',range(10))
p = Process(target= f, args=(num,arr))
p.start()
p.join()
print num.value
print arr[:]
#输出
3.1415727
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
协程
协程又叫做微线程,英文名Coroutine,从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。线程和进程的操作是由程序触发系统接口,最后的执行者是系统。协程的操作则是程序员。
线程是系统级别的,它们是由操作系统调度。协程是程序级别的,由程序员根据需要自己调度。我们把一个线程中一个个函数叫做子程序,那么子程序在执行过程中可以中断去执行别的子程序;别的子程序也可以中断回来继续执行之前的子程序,这就是协程。也就是说同一线程下的一段代码<1>执行着就可以中断,然后跳去执行另一段代码,当再次回来执行代码块<1>的时候,接着从之前中断的地方开始执行。
比较专业的理解是:协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:
def A():
print '1'
print '2'
print '3'
def B():
print 'x'
print 'y'
print 'z'
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:
1
2
x
y
3
z
但是,在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。
看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那么怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程。
Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。
注意:
1.示例如下,
>>> l = (i for i in range(1,10))
>>> l
<generator object <genexpr> at 0x00000000025003A8>
2.yield可以去阻断当前的函数执行。然后,当使用next()函数,或者__next__(),都会让函数继续执行。当执行到下一个yield语句的时候,又会被暂停。
来看例子,传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'
def produce(c):
c.next()
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
if __name__=='__main__':
c = consumer()
produce(c)
执行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:
1.首先调用c.next()启动生成器;
2.然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
3.consumer通过yield拿到消息,处理,又通过yield把结果传回;
4.produce难道consumer处理的结果,继续生产下一条消息;
5.produce决定不生产了,通过c.close()关闭consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其它的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。
使用gevent,可以获得极高的并发性能,但gevent只能在Unix/Linux下运行,在Windows下不保证正常安装和运行。由于gevent是基于IO切换的协程,所以最神奇的是,我们编写的Web App代码,不需要引入gevent的包,也不需要改任何代码,仅仅在部署的时候,用一个支持gevent的WSGI服务器,立刻就获得了数倍的性能提升。
greenlet
gevent
Daemon Vs. Non-Daemon Threads
Up to this point, the example programs have implicitly waited to exit until all threads have completed their work.Sometimes programs spawn a thread as a daemon that runs without blocking the main program from exiting. Using daemon threads is useful for services where there may not be any easy way to interrupt the thread or where letting the thread die in the middle of its work does not lose or corrupt data(for example, a thread that generates "heart beats" for a service monitoring tool). To mark a thread as a daemon, call its setDaemon() method with a boolean argument. The default is for threads to not be daemons, so passing True turns the daemon mode on.
import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
def daemon():
logging.debug('Starting')
time.sleep(2)
logging.debug('Exiting')
d = threading.Thread(name='daemon', target=daemon)
d.setDaemon(True)
def non_daemon():
logging.debug('Starting')
logging.debug('Exiting')
t = threading.Thread(name='non-daemon', target=non_daemon)
d.start()
t.start()
#输出
(daemon ) Starting
(non-daemon) Starting
(non-daemon) Exiting
Notice that the output does not include the "Exiting" message from the daemon thread, since all of the non-daemon threads(including the main thread) exit before the daemon thread wakes up from its two second sleep.
To wait until a daemon thread has completed its work, use the join() method.Waiting for the daemon thread to exit using join() means it has a chance to produce its "Exiting" message.
import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
def daemon():
logging.debug('Starting')
time.sleep(2)
logging.debug('Exiting')
d = threading.Thread(name='daemon', target=daemon)
d.setDaemon(True)
def non_daemon():
logging.debug('Starting')
logging.debug('Exiting')
t = threading.Thread(name='non-daemon', target=non_daemon)
d.start()
t.start()
d.join()
t.join()
#输出
(daemon ) Starting
(non-daemon) Starting
(non-daemon) Exiting
(daemon ) Exiting
By default, join() blocks indefinitely.It is also possible to pass a timeout argument(a float representing the number of seconds to wait for the thread to become inactive).If the thread does not complete within the timeout period,join() returns anyway.
Since the timeout passed is less than the amount of time the daemon threads sleeps, the threads is still "alive" after join() returns.
import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,
format='(%(threadName)-10s) %(message)s',
)
def daemon():
logging.debug('Starting')
time.sleep(2)
logging.debug('Exiting')
d = threading.Thread(name='daemon', target=daemon)
d.setDaemon(True)
def non_daemon():
logging.debug('Starting')
logging.debug('Exiting')
t = threading.Thread(name='non-daemon', target=non_daemon)
d.start()
t.start()
d.join(1)
print 'd.isAlive()', d.isAlive()
t.join()
#输出
(daemon ) Starting
(non-daemon) Starting
(non-daemon) Exiting
d.isAlive() True
[https://pymotw.com/2/threading/#daemon-vs-non-daemon-threads]
进程 Vs. 线程
我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,来讨论一下这两种方式的优缺点。
首先,要实现多任务,通常会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Wroker。
如果用多进程实现Master-Worker,主进程就是Master,其它进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其它线程就是Worker。
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程的其它子进程。(当然主进程挂掉了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低),著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程的崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式。
multiprocessing.Queue()和Queue.Queue()区别
Queue.Queue是进程内非阻塞队列。multiprocessing.Queue是跨进程通信队列。
多进程中,前者是各自私有,后者是各子进程共有。
线程切换
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
计算密集型 Vs IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
异步IO
考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。
现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。
对应到Python语言,单进程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。我们会在后面讨论如何编写协程。
多核CPU
如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。
如果写一个死循环的话,会出现什么情况呢?打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。我们可以监控到一个死循环线程会100%占用一个CPU。
如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。
要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。
试试用Python写个死循环:
import threading, multiprocessing
def loop():
x = 0
while True:
x = x ^ 1
for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()
启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有160%,也就是使用不到两核。
即使启动100个线程,使用率也就170%左右,仍然不到两核。
但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?
因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。
不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。
Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
远程分布式主机(Distributed Node)
随着大数据时代的来临,摩尔定律在单机上似乎已经失去了效果,数据的计算和处理需要分布式的计算机网络来运行,程序并行的运行在多个主机节点上,已经是现在的软件架构所必须考虑的问题。
远程主机间的进程间通信有几种常见的方式,
1.TCP/IP
TCP/IP是所有远程通信的基础,然而API比较低级别,使用起来比较繁琐,所以一般不会考虑。
2.远程过程调用 Remote Procedure Call
RPC是早期的远程进程间通信的手段。Python下有一个开源的实现RPyC。
3.远程对象 Remote Object
远程对象是更高级别的封装,程序可以像操作本地对象一样去操作一个远程对象在本地的代理。远程对象最广为使用的规范CORBA,CORBA最大的好处是可以在不同语言和平台中进行通信。当然,不同的语言和平台还有一些各自的远程对象实现,例如Java的RMI,MS的DCOM
Python的开源实现,有许多对远程对象的支持,
Dopy
Fnorb (CORBA)
ICE
omniORB (CORBA)
Pyro
YAMI
4.消息队列 Message Queue
比起RPC或者远程对象,消息队列是一种更为灵活的通信手段,常见的支持Python接口的消息机制有,
RabbitMQ
ZeroMQ
Kafka
AWS SQS + BOTO
在远程主机上执行并发和本地的多进程并没有非常大的差异,都需要解决进程间通信的问题。当然对远程进程的管理和协调比起本地要复杂。
Python下有许多开源的框架来支持分布式的并发,提供有效的管理手段包括:
1.Celery
Celery是一个非常成熟的Python分布式框架,可以在分布式的系统中,异步的执行任务,并提供有效的管理和调度功能。
2.SCOOP
SCOOP (Scalable COncurrent Operations in Python)提供简单易用的分布式调用接口,使用Future接口来进行并发。
3.Dispy
相比起Celery和SCOOP,Dispy提供更为轻量级的分布式并行服务。
4.PP
PP(Parallel Python)是另外一个轻量级的Python并行服务。
5.Asyncoro
Asyncoro是另一个利用Generator实现分布式并发的Python框架。
当然还有许多其它的系统,这里没有一一列出。
另外,许多的分布式系统多提供了对Python接口的支持,例如Spark。