python多进程-----multiprocessing包

multiprocessing并非是python的一个模块,而是python中多进程管理的一个包,在学习的时候可以与threading这个模块作类比,正如我们在上一篇转载的文章中所提,python的多线程并不能做到真正的并行处理,只能完成相对的并发处理,那么我们需要的就是python的多进程来完成并行处理,把所有的cpu资源都利用起来。multiprocessing的很大一部分与threading使用同一套API,只不过换到了多进程的环境。这里面要注意,对于多进程来说,win32平台和unix平台差别很大,我们最好在linux上完成实现。

使用这些共享API时,我们应该注意以下问题(目前这是我能想到的,以后遇到再扩充):

1、对join的处理

根据Unix环境高级编程中对进程控制一章的描述,当某个进程fork一个子进程后,该进程必须要调用wait等待子进程结束发送的sigchld信号,对子进程进行资源回收等相关工作,否则,子进程会成为僵死进程,被init收养。所以,在multiprocessing.Process实例化一个对象之后,该对象有必要调用join方法,因为在join方法中完成了对底层wait的处理,源码如下:

    def join(self, timeout=None):
'''
Wait until child process terminates
'''
assert self._parent_pid == os.getpid(), 'can only join a child process'
assert self._popen is not None, 'can only join a started process'
res = self._popen.wait(timeout)
if res is not None:
_current_process._children.discard(self)

不过,调用该方法,要注意join的位置(threading模块有提到),是在每个子进程中阻塞还是在父进程中阻塞,如果在子进程中阻塞可能达不到并行处理的目的,所以要根据具体需求。而对于多线程来说,由于只有一个进程,所有子线程共享同一片内存,所以不是必须要进行join调用。例子如下:

#!/usr/bin/env python
__author__ = 'webber'
import os,time
import multiprocessing # worker function
def worker(sign, lock):
lock.acquire()
print sign, 'pid:',os.getpid()
lock.release()
time.sleep(1) # Main
print 'Main:',os.getpid() plist = []
lock = multiprocessing.Lock()
for j in range(5):
p = multiprocessing.Process(target=worker,args=('process',lock))
p.start()
plist.append(p)
p.join() #for process in record:
# process.join()

此外,还有一点关于GIL锁的说明,在python多进程中,同样需要全局解释器锁,因为每个子进程都有一把GIL,那么当它们向stdout输出时,可以同时争抢stdout资源,导致在每个子进程输出时会把不同子进程的输出字符混合在一起,无法阅读,影响代码的标志位判断,所以上例子中使用了Lock同步,在一个子进程输出完成之后再允许另一个子进程得到stdout的权限,这样避免了多个任务同时向终端输出。

2、对IPC的处理

multiprocessing包与threading模块的另一个差异特性体现在IPC上,python的multiprocessing包自带了对Pipe和Queue的管理,效率上更高,而threading模块需要与Queue模块或os.popen()、subprocess.Popen()等配合使用。
      根据Unix环境高级编程的第15章进程间通信的描述,经典的IPC包括管道、FIFO、消息队列、信号量、以及共享存储。不过应用最多的还是管道。书中指出我们应该把管道看成是半双工的,并且只能在具有公共祖先的两个进程之间使用。
下面我们用一下Pipe()和Queue()方法:

  a、关于Pipe()

对照书中给出的底层pipe接口函数,我们看到Pipe方法在Unix平台上实现源码如下:

def Pipe(duplex=True):
'''
Returns pair of connection objects at either end of a pipe
'''
if duplex:
s1, s2 = socket.socketpair()
s1.setblocking(True)
s2.setblocking(True)
c1 = _multiprocessing.Connection(os.dup(s1.fileno()))
c2 = _multiprocessing.Connection(os.dup(s2.fileno()))
s1.close()
s2.close()
else:
fd1, fd2 = os.pipe()
c1 = _multiprocessing.Connection(fd1, writable=False)
c2 = _multiprocessing.Connection(fd2, readable=False) return c1, c2

首先,Pipe可以是单向(half-duplex),也可以是双向的(duplex),默认为双向的。我们可以通过multiprocessing.Pipe(duplex=False)创建单向的管道。该方法返回一个元祖,包含两个文件描述符,如果为单向的,则为(read-only connection,write-only connection);如果为双向的,则为(read-write Connection, read-write Connection)。一个进程从Pipe一端输入对象(fd[1]),然后被Pipe另一端的进程接收(fd[0]),两个进程要有同一个父进程或者其中一个是父进程。单向管道只允许管道一端的进程输入,而双向管道则允许从两端输入。这里的双向管道类似于书中提到的“协同进程”的概念。
例如:

#!/usr/bin/env python
import multiprocessing as mul def proc1(pipe):
# pipe.send('hello')
print 'proc1 rec:',pipe.recv() def proc2(pipe):
# print 'proc2 rec:',pipe.recv()
pipe.send('hello too') pipe = mul.Pipe(duplex=False)
#pipe = mul.Pipe() p1 = mul.Process(target=proc1,args=(pipe[0],)) # 读管道 p2 = mul.Process(target=proc2,args=(pipe[1],)) # 写管道
# 由于管道是单向的,对象pipe[0]只有读的权限(recv),而pipe[1]只有写的权限(send)。
#print pipe
p1.start()
p2.start()
p1.join()
p2.join()

b、关于Queue()

Queue与Pipe相类似,都是先进先出的结构,但Queue允许多个进程放入,多个进程从队列取出对象。这里可以与Queue模块相类比学习。Queue方法其实是Unix环境高级编程IPC中FIFO命名管道的实现方法。FIFO可用于有以下两种情况:
---shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件。
---客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间传递数据。
以下就FIFO的第二种情况写一个python例子:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import multiprocessing
import time
import os # 客户进程,向众所周知的FIFO服务器进程发送请求
def client_proc(queue,msg):
request = 'I am client ' + str(msg) + ' pid: '+ str(os.getpid()) + ' time:' + str(time.time()) # 注意信息的格式,都统一为字符串类型
queue.put(request) def server_proc(queue,lock):
msg = queue.get()
lock.acquire()
print msg + '--------------->I am server ' + 'pid: ' + str(os.getpid())
lock.release() plist_cli = []
plist_ser = []
lock = multiprocessing.Lock()
queue = multiprocessing.Queue() # 参数为空,默认为队列可无限长 for i in range(10):
p1 = multiprocessing.Process(target=client_proc,args=(queue,i))
p2 = multiprocessing.Process(target=server_proc,args=(queue,lock))
p1.start()
p2.start()
plist_cli.append(p1)
plist_ser.append(p2) for proc in plist_cli:
proc.join() for proc in plist_ser:
proc.join() queue.close()
输出如下:
I am client 2    pid: 9867   time:1482489226.77--------------->I am server pid: 9879
I am client 0    pid: 9865   time:1482489226.77--------------->I am server pid: 9881
I am client 4    pid: 9869   time:1482489226.77--------------->I am server pid: 9884
I am client 1    pid: 9866   time:1482489226.77--------------->I am server pid: 9886
I am client 3    pid: 9868   time:1482489226.78--------------->I am server pid: 9888
I am client 7    pid: 9872   time:1482489226.78--------------->I am server pid: 9889
I am client 5    pid: 9870   time:1482489226.78--------------->I am server pid: 9892
I am client 6    pid: 9871   time:1482489226.78--------------->I am server pid: 9891
I am client 9    pid: 9878   time:1482489226.78--------------->I am server pid: 9893
I am client 8    pid: 9875   time:1482489226.78--------------->I am server pid: 9894

从输出可以看出,10个客户端进程把生产信息放入队列,10个服务端进程从队列取出信息并且打印,从打印时间和msg的子进程编号来看,10个服务端进程争夺stdout,通过Lock使它们有序输出,不至于输出信息混乱,msg编号没有从0排至9正是因为它们被分配给了不同的cpu资源,不同cpu资源在处理速度上不会完全一样,所以争夺stdout的能力也不同。

3、共享内存和Manager管理

众所周知,在处理多进程时,每个进程都有自己独立的内存空间,所以在多进程环境中我们应该尽量避免共享资源,否则要依赖与IPC。python的多进程除了上面提到的常用的依赖于管道和FIFO之外,还可以通过共享内存和Manager的方法来共享资源。这个不常用,由于共享内存涉及同步的问题,会降低程序的效率而不推荐使用。以后涉及到再扩展。

4、进程池

参考博客:http://www.cnblogs.com/kaituorensheng/p/4465768.html
当我们在编写网络服务端时,Unix网络编程一书中提到服务端需要fork子进程,用子进程来处理监听到的连接请求,建立连接套接字,并在子进程中关闭监听套接字,父进程中关闭连接套接字。那么,当连接的并发不是很大时,我们可以利用进程池的方式来处理到来的连接。multiprocessing.Pool可以提供指定数量的进程供用户调用,当有新的请求提交到pool中时,如果进程池还没有满,那么就会创建一个新的进程用来执行该请求;如果池中的进程数已经达到最大值,那么该请求将会阻塞等待,直到池中有进程结束,才会创建新的进程来处理该请求。
Pool方法默认的初始值如下:
def __init__(self, processes=None, initializer=None, initargs=(),maxtasksperchild=None)
通常,我们应该指定进程池的大小,如果不指定,默认为cpu的个数,即processes=cpu_count(),我们可以用该模块自带的方法查看本机的cpu个数,

print multiprocessing.cpu_count()。下面看个进程池的例子:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import multiprocessing
import time def func(msg):
print 'msg:',msg
time.sleep(3)
print 'end' pool = multiprocessing.Pool(processes=3)
for i in xrange(4):
msg = 'hello %d' % (i)
pool.apply_async(func,(msg,)) #非阻塞
# pool.apply(func,(msg,)) #阻塞,apply()源自内建函数,用于间接的调用函数,并且按位置把元祖或字典作为参数传入。 # pool.imap(func,[msg,]) #非阻塞, 注意与apply传的参数的区别
# pool.map(func,[msg,]) #阻塞 print 'Mark~~~~~~~~~~~~~~~'
pool.close()
pool.join() # 调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
print 'sub-process done'

注意apply_async和apply的差别,此外,进程池请求函数处理还可以用map,imap,注意传递参数的区别。

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import multiprocessing
import time def func(msg):
print 'msg:',msg
time.sleep(3)
print 'end' pool = multiprocessing.Pool(50)
msg = range(50)
#pool.imap(func,msg) #非阻塞, 注意与apply传的参数的区别
pool.map(func,msg) #阻塞 print 'Mark~~~~~~~~~~~~~~~'
pool.close()
pool.join()
print 'sub-process done'

此外,如果子进程的处理函数中包含返回值,我们可以在父进程中对子进程调用get方法,将返回值取出,这里注意,要调用get方法的时候,进程池必须采用apply_async调用函数。例如:

if __name__ == "__main__":
pool = multiprocessing.Pool(processes=4)
result = []
for i in xrange(3):
msg = "hello %d" %(i)
result.append(pool.apply_async(func, (msg, )))
pool.close()
pool.join()
for res in result:
print ":::", res.get()
print "Sub-process(es) done."

最后,调用close()之后,进程池不再创建新的进程;

调用join()之后,wait进程池中的全部进程。必须对Pool先调用close()方法才能join。

参考博客:http://www.lxway.com/4488626156.htm

上一篇:python学习笔记——multiprocessing 多进程组件 进程池Pool


下一篇:python 3 并发编程之多进程 multiprocessing模块