一、关于进程锁。
其实关于进程锁没啥好讲的了,作用跟线程的互斥锁(又叫全局锁也叫同步锁)作用几乎是一样的。
都是用来给公共资源上锁,进行数据保护的。
当一个进程想去操作一个公共资源,它就可以给公共资源进程“上锁”的操作,其他进程如果也想去访问或者操作这个公共资源,那么其他的进程只能阻塞,等待刚刚的进程把锁释放,下一个进程才可以对这个公共资源进行操作。
下面是个关于进程锁的使用示范:
#!/usr/local/bin/python2.7
# -*- coding:utf-8 -*-
import multiprocessing
def func1(lock_1,name):
lock_1.acquire()
print "hello %s" %(name) #当执行到这句话的时候,就不再是并行了,和线程中锁的概念是一样的。
lock_1.release()
if __name__ == ‘__main__‘:
lock = multiprocessing.Lock()
for num in range(10):
multiprocessing.Process(target=func1,args=(lock,num)).start()
输出结果:
hello 0
hello 1
hello 2
hello 3
hello 4
hello 5
hello 6
hello 7
hello 8
hello 9
!!补充下,其实在进程中,也可以使用RLock递归锁,信号量,event等各种锁,用法基本和多线程是一样的。
二、关于进程池。
什么是进程池呢?
进程池就是程序中维护的一个进程序列,当需要使用进程的时候,就会从进程池中获取一个进程,当进程池中不再有进程的时候,程序就会阻塞,一直等到有新的进程加入到进程池为止。
那么进程池有什么好处?可以看看下面这个例子。
假如说有个函数,这个函数的内容需要被执行100次,才能完成我们想要完成的任务。
我们假定运行一次这个函数要10秒钟,如果使用单进程单线程的模式串行执行,执行这个函数100次要花费1000秒的时间,效率非常低!
那么多线程呢?我们可以同时开100个线程,每个线程去执行一次这个函数,也很快可以执行结束,但是前提是,这个函数所执行的操作必须属于I/O密集型,这样才可以发挥出多线程的高效率,如果这个函数所执行的操作是计算密集型的呢?(如果计算密集型的任务交给多线程去做,反而可能会降低效率,这是因为全局解释器锁的特性造成的)那我们就需要去考虑去开多进程了。
好,那接下来我们用多线程的方式去处理这个问题。
这个函数既然要执行100次,那么我干脆去开100个进程,让这100个函数并行去执行这个函数。
这样确实可以大大提高效率,但是,无论是开100个进程也好,或者说是开100个线程也好,这样做开销实在是太大了!!!同一个进程里面的多个线程还好,数据资源在多个进程之间都是共享的,线程和线程之间每次切换,值需要保存当前的执行状态就可以了,但是线程呢,线程和线程之间的数据都是独立的,多个进程要想实现数据共享,就要给每个进程复制一份数据资源,开100个线程,数据资源就要被复制100份!开销实在太大了!
其实上面的这些方法都不太好,但是可以折中考虑,创建一个线程池。
接下来我们来分析下进程池的好处。
假设说,我们创建了一个进程池,创建这个进程池的时候,指定这个进程池中最多可以容纳10个进程,也就意味着一次的最大并发是10个进程一起去执行这个函数。
接下来,这10个进程都把自己的事情做完了,当然,这10个进程不可能完全是一起执行完毕的(除非你的cpu有十核),肯定有先执行完任务进程。
那么换种说法吧,这10个并发运行的进程中,现在假如有一个进程已经运行完毕,那么这个进程池中还剩下9个进程,我的进程池中现在空出来了一个位置,空出来的这个位置,还可以继续开一个新的进程去工作(只要进程池中有空位,就可以开启新的进程去继续工作。)
所以说,通过以上的例子,可以明确一点,进程池的好处,就是可以维护一个进程工作的最大并发量,比如我们手动设置进程池中最大可以容纳10个进程,那么在程序运行时,运行的进程数量不会高于10个,也不会低于10个,可以一直维持这个最大并发量。
虽然没办法10秒执行100次这个函数,但是,使用了进程池,肯定要比串行执行要快的多!
所以说,进程池是一个折中的解决方案。
下面是进程池的使用示例:
#!/usr/local/bin/python2.7
# -*- coding:utf-8 -*-
import multiprocessing
import os
import time
def func1(value):
time.sleep(5)
print "---------->%s" %(value)
return value+100
def display_pro_info(value):
print os.getpid()
print os.getppid()
print "log:%s" %(value)
pro_pool = multiprocessing.Pool(10) #创建线程池对象,并指定并最大并发量,如果不指定,会按照当前计算机的cpu核数来自动调整.
display_pro_info(1)
print "--------------->"
for i in range(100): #创建100个线程
# pro_pool.apply_async(func=func1,args=(i,)) #这一步可以暂时理解为往进程池里添加进程,本文后面会对该步骤做详细描述。
pro_pool.apply_async(func=func1,args=(i,),callback=display_pro_info) #这里使用了callback回调函数,什么是回调函数,本文后面会介绍。
pro_pool.close() #在这需要注意一点,就是使用进程池,一定要先close再join,否则就会报错。先close后join这个顺序是固定的!!
pro_pool.join() #没有join的话,进程池里的进程是不会运行的。
print "the end!"
下面输出一下这个程序的运行结果:
44720
40833
log:0
--------------->
---------->0
---------->1
---------->3
---------->2
---------->5
---------->9
---------->4
---------->7
---------->6
---------->8
44720
40833
log:100
44720
40833
log:101
44720
40833
log:103
44720
40833
log:102
44720
40833
log:109
44720
40833
log:104
44720
40833
log:107
44720
40833
log:106
44720
40833
log:105
44720
40833
log:108
......
the end!
下面开始对代码进行分析,并且对进程池中一些常用的方法进行讲解:
- 首先来说说,在进程池中添加进程的两种模式。
分别是apply(同步执行模式)和apply_async(异步执行模式)。
1.1 apply(同步执行模式)
这种模式一般情况下没人会去用,如果进程池使用了这种模式,当进程池的第一个进程执行完毕后,才会执行第二个进程,第二个进程执行完毕后,在执行第三个进程....(也就是说这种模式会阻塞主进程!),无法实现并行效果。(不止如此,这种模式还不支持callback回调函数。)
1.2 apply_async (异步执行模式)
异步的执行模式,才是可以实现并行效果的模式,支持callback回调函数,当一个进程没有执行完毕,没有返回结果,异步执行的模式并不会对主进程进行阻塞!
补充一点!虽然 apply_async是非阻塞的,但其返回结果的get方法却是阻塞的,如使用result.get()会阻塞主进程!
1.3 pro_pool.apply_async(func=func1,args=(i,),callback=display_pro_info)
语法和创建多线程多进程没有什么差别,func用于指定一个进程要运行的函数,args用来给函数传参数,callback用来指定回调函数。
2.关于join,close,terminate。
2.1 join 主进程阻塞等待子进程运行结束后退出, join方法要在close或terminate之后使用。(换种说法就是,不加join方法,进程池里的进程根本不会执行~)
2.2 close 关闭进程池,不再接受新的任务
2.3 terminate 直接结束进程,不再处理没有处理的任务
#其实join和close比较常用。
3.关于callback函数的一个简单说明。
在了解回调函数之前,需要注意!!callback函数是由主进程去调用的,并不是子进程!!从刚刚的示例中就可以看出结果!
某一个函数执行完毕后,某个函数或者动作执行成功后,再去执行的一个函数,并且之前执行成功的那个函数的返回值,会作为参数传给callback函数。
让回调函数在主进程下调用有什么好处?
比如现在需要开10个线程去对数据库中的数据进行操作,现在有个需求,就是对数据库内部进行的操作都需要记录一个日志,我们可以把写日志这个函数做为一个回调(callback)函数。
在不使用callback函数之前,如果想去开10个进程去操作数据库,这10个进程是并发执行的,每个进程都去操作数据库后,同时去写一个日志文件,如果不加锁的话很容易造成日志文件的损坏,所以我们可以把写日志的函数去交给主进程去执行,当进程池中的10个进程运行结束后,主线程直接执行一个写日志的操作,这就是我理解的callback函数的用处。
拿进程池中的callback来举例吧,pro_pool.apply_async(func=func1,args=(i,),callback=display_pro_info)当func1函数内部执行成功后(只要返回值不为假),display_pro_info这个函数就会被作为回调函数去执行(当然执行这个函数的是主进程!),func1的返回值会作为参数传给display_pro_info这个函数。
最后补充一下,使用callback回调函数需要注意的几点。
- 在执行回调函数之前的那个函数,必须有一个返回值!这个返回值有两个用途,第一个用途是判断这个函数是否执行成功,执行成功才可以执行callback函数,另一个用途就是这个返回值会作为参数传给回调函数。
- callback回调函数本身必须接收一个参数,这个参数用来接收上一个函数执行完毕后的返回值。