多进程
多进程比起多线程更加稳定,如果一个进程死掉了,并不会影响别的进程,但如果一个线程挂了,整个程序就挂了,当然,主进程要是挂了也没办法,python中multiprocessing用来支持多线程编程,他提供了Process、Queue、Pipe、Lock等组件
1.进程的创建与管理
Process类用来创建和管理进程,另外,可以使用Pool进行进程并发操作,调用Process()时必须放在if __name__ == '__main__':
下面,否则在python3下会报错,在python2下会一直创建进程,直到电脑资源耗尽,这个的原因emmmmmm,首先if __name__ == '__main__':
是python的每一个模块都肯定有一个__name__
属性,对他程序自己来说,他的__name__
就是__main__
而从外部导入的模块代码运行时就不是了,如果不加这一句,外部导入模块语句就会被重复执行,进程就会被一直创建,python3中对进程创建进程有限制,所以会报错。
1.1 Process类
1.1.1 Process类的属性和方法
class Process():
name: str
daemon: bool
pid: Optional[int]
exitcode: Optional[int]
authkey: bytes
sentinel: int
# TODO: set type of group to None
def __init__(self,
group: Any = ...,
target: Optional[Callable] = ...,
name: Optional[str] = ...,
args: Iterable[Any] = ...,
kwargs: Mapping[Any, Any] = ...,
*,
daemon: Optional[bool] = ...) -> None: ...
def start(self) -> None: ...
def run(self) -> None: ...
def terminate(self) -> None: ...
if sys.version_info >= (3, 7):
def kill(self) -> None: ...
def close(self) -> None: ...
def is_alive(self) -> bool: ...
def join(self, timeout: Optional[float] = ...) -> None: ...
属性介绍:
看源码有六个属性,name就是进程名,daemon是是否是守护进程,pid是进程对应的pid,exitcode好像是与进程运行有关的,进程正在运行他的值是None,要是进程被某个信号结束,这个属性可以显示哪个信号,后面那个authkey更不懂了,字面意思就是自动生成的一个key,他与网络连接底层进程间通信的安全有关,是一个32位字符串,sentinel,哨兵,似乎与定时发送消息有关…
参数介绍:
- group: 未使用,None
- target:调用对象,与线程中的一样
- name:进程名,不指定会自动创建
- args:多值元组
- kwargs:多值字典
- daemon:为True时创建守护进程
方法介绍:
- start:启动进程
- run:启动进程,这是真的启动,start也是调用子进程的run方法才启动的,用类创建进程时就要重写run方法
- terminate:强制终止这个进程,不做清理,如果该进程创建了子进程,子进程就会变成僵尸进程,如果这个进程还有一个锁没有释放,就会导致死锁
- kill:直接杀死进程
- close:关闭进程池,关闭之后就不允许别的Process加入了
- is_alive:判断进程还活着没
- join:阻塞主进程等待该进程结束
1.1.2 创建子进程的两种方法
(1.)使用函数创建
# 使用函数创建进程
from multiprocessing import Process
import time
import os
def demo(x,y):
StartTime=time.time()
print('%s start ...'%os.getpid())
print(x+y)
time.sleep(2)
print('%s end runs %0.2f s'%(os.getpid(),(time.time()-StartTime)))
if __name__ == '__main__':
s=Process(target=demo,args=(1,2))
d=Process(target=demo,args=(3,4))
s.start()
d.start()
print('main')
结果
main
28956 start ...
3
21140 start ...
7
28956 end runs 2.00 s
21140 end runs 2.00 s
(2.)使用类创建
使用类创建就是重写Process类,必须继承Process类
# 使用类创建进程
from multiprocessing import Process
import time
import os
class MyProcess(Process):
'''重写process类'''
def __init__(self,x,y):
super().__init__()
self.x=x
self.y=y
def run(self):
StartTime=time.time()
print('%s start'%self.name)
print(self.x+self.y)
time.sleep(2)
print('%s end runs %0.2f s' % (self.name, (time.time() - StartTime)))
if __name__ == '__main__':
q=MyProcess(1,2)
d=MyProcess(3,4)
q.start()
d.start()
运行结果
MyProcess-1 start
3
MyProcess-2 start
7
MyProcess-1 end runs 2.00 s
MyProcess-2 end runs 2.00 s
1.1.3 僵尸进程和孤儿进程
这两个一般针对Linux来说,Windows进程间父子关系很弱,进程自生自灭,不存在所谓僵尸进程
(1.)僵尸进程
由于父进程和主进程是异步的,所以父进程不会知道主进程会在什么时候结束,因此,为了让子进程结束后让父进程知道,子进程结束后会保留一部分系统资源如pid,运行时间,退出状态等,等父进程通过wait()或者waitpid()系统调用取得这些信息时,这部分资源才会被释放,但如果父进程一直未调用wait()或者waitpid(),那这些资源就一直不会被释放,比如pid,pid的数量是有限的,如果僵尸进程过多,就会导致pid不足而无法创建新进程,所以僵尸进程是有害的。
# 僵尸进程
from multiprocessing import Process
import time
import os
def demo():
StartTime=time.time()
print('%d process start,father is %d time is %.2f'%(os.getpid(),os.getppid(),StartTime))
time.sleep(1)
print('%d process end father is %d time is %.2f'%(os.getpid(),os.getppid(),time.time()))
if __name__ == '__main__':
p=Process(target=demo)
print('father process is %d'%os.getpid())
p.start()
time.sleep(10000)
print('father process is %d' % os.getpid())
运行之后,子进程1s结束,主进程延时10000,子进程就会变成僵尸进程,通过ps aux|grep Z
(使用通道查看所有与Z有关的系统进程)可以发现这个僵尸进程(第一个)
junbao@ubuntu:~$ ps aux |grep Z
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
junbao 24234 0.0 0.0 0 0 pts/0 Z+ 22:12 0:00 [python3] <defunct>
junbao 24259 0.0 0.0 21536 1104 pts/1 S+ 22:13 0:00 grep --color=auto Z
处理僵尸进程的办法一是杀死父进程,父进程结束后僵尸进程就会变成孤儿进程,由init收养后释放资源。二是使用signal(SIGCHLD, SIG_IGN)处理僵尸进程,父进程在接收到子进程退出的信号后,直接将其忽略掉,类似于父进程先退出。
(2.)孤儿进程
如果父进程终止,而由父进程创建的一个或多个子进程还在执行的话,这一个或多个子进程就会成为孤儿进程,成为孤儿进程后,善后工作(wait()或者waitpid()等)就会由init接管,init进程是内核启动的第一个进程,pid=1,由0号进程idle创建。应为有人善后,所以孤儿进程是无害的。
1.2 进程并行
如果需要创建大量进程,就可以使用Pool,他是multiprocessing模块的一个函数,进程初始化时,会指定一个最大进程数processes,一般是GPU核数,如果有新的进程请求被创建时,如果进程池未满,就会创建该进程并加入进程池,如果进程池已满,该请求就会被延时,直到有某个进程结束后释放进程池空间,才会被执行。但不是最大进程数指定越多运行越快,进程会占用系统资源,一般指定为系统核数,多出来的指定了操作系统也无法执行,反而会降低程序效率。
def Pool(processes: Optional[int] = ...,
initializer: Optional[Callable[..., Any]] = ...,
initargs: Iterable[Any] = ...,
maxtasksperchild: Optional[int] = ...) -> pool.Pool: ...
参数简介:
- processes:要创建的最大进程数,省略默认为GPU核数
- initializer:每个进程启动时都默认执行的可调用对象
- initargs:传入给initializer的参数
- maxtasksperchild:允许创建的最大子进程数
方法简介
- close:关闭进程池,不接受新进程,但已运行的进程会继续
- join:阻塞主进程,必须在close 或terminate之后运行
- terminate:不管进程执行结果,直接结束
一个进程只有三种状态(_state
),RUN,CLOSE,TERMINATE,运行,关闭,终止,这是三个全局变量
RUN = 0
CLOSE = 1
TERMINATE = 2
close 和 terminate做的工作就是修改_state
和处理器状态_worker_handler._state
- apply:相当于
func(* args,** kwds)
。Pool必须运行,阻塞式。 - apply_async:与apply类似,只不过是非阻塞式的,阻塞式和非阻塞式的区别在于阻塞式是进入子进程后等待当前子进程结束后再调用下一个子进程,这样其实就和单进程串行没什么区别,而非阻塞是不用等待当前子进程结束就按系统调度直接调用。
apply
# Pool
from multiprocessing import Process,Pool
import time
def demo(x):
print('start ..%d'%x)
time.sleep(2)
print('%d end'%x)
if __name__ == '__main__':
print('main start')
starttime=time.time()
P=Pool(4)
for i in range(4):
P.apply(demo,args=(i,))
P.close()
P.join()
print('main end uned time is %.2lf'%(time.time()-starttime))
# main start
# start ..0
# 0 end
# start ..1
# 1 end
# start ..2
# 2 end
# start ..3
# 3 end
# main end uned time is 8.78
apply_async
# Pool
from multiprocessing import Process,Pool
import time
def demo(x):
print('start ..%d'%x)
time.sleep(2)
print('%d end'%x)
if __name__ == '__main__':
print('main start')
starttime=time.time()
P=Pool(4)
for i in range(4):
P.apply_async(demo,args=(i,))
P.close()
P.join()
print('main end uned time is %.2lf'%(time.time()-starttime))
# main start
# start ..0
# start ..1
# start ..2
# start ..3
# 0 end
# 1 end
# 2 end
# 3 end
# main end uned time is 2.78
- map和map_async: 将
func
应用于iterable
中的每个元素,收集结果在返回的列表中。map_async与map不同之处是他是异步的 - starmap和starmap_async: 像
map()
方法一样,但iterable
的元素应该是也可以迭代,并将作为参数解压缩。于是func
和(a,b)变为func(a,b)。 - imap: Equivalent of
map()
– can be MUCH slower thanPool.map()
.其实他返回了一个生成器对象,看源码->return (item for chunk in result for item in chunk)
- imap_unordered: Like
imap()
method but ordering of results is arbitrary.(和imap一样,只不过排序是任意的)
1.3 守护进程
一个进程被设置为守护进程时,会随着主进程结束而立即结束,守护进程不能再创建子进程
# 使用类创建进程
from multiprocessing import Process
import time
import os
def demo():
print('孙子进程启动:pid:%d 父进程: %d'%(os.getpid(),os.getppid()))
time.sleep(20)
print('孙子进程结束:pid:%d 父进程: %d'%(os.getpid(),os.getppid()))
class MyProcess(Process):
def __init__(self,x,y):
super().__init__()
self.x=x
self.y=y
def run(self):
print('子进程启动 pid %d ,父进程:%d' % (os.getpid(),os.getppid()))
a=Process(target=demo)
# 设置守护进程
a.daemon=True
a.start()
time.sleep(5)
print('子进程结束 pid:%d ,父进程:%d' % (os.getpid(),os.getppid()))
if __name__ == '__main__':
print('主进程启动,pid: %d'%os.getpid())
q=MyProcess(1,2)
q.start()
print('主进程结束 pid: %d ' % os.getpid())
# 主进程启动,pid: 14584
# 主进程结束 pid: 14584
# 子进程启动 pid 18616 ,父进程:14584
# 孙子进程启动:pid:20728 父进程: 18616
# 子进程结束 pid:18616 ,父进程:14584
进程同步
多个进程之间虽然不共享内存,但是共享输出啊,共享文件啊,如果多个进程同时修改一个文件,不就乱了吗,,,所以要用进程同步,也就是锁,只有拿到锁的进程才可以执行,别的只能阻塞,锁的操作和多线程中线程锁是一样的。用完锁一定要释放锁。
# 进程锁
import time
from multiprocessing import Process,Lock
def demo(lock,name):
lock.acquire()
for i in range(10):
time.sleep(1)
print('%d---%s'%(i,name))
lock.release()
def demo2(lock,name):
lock.acquire()
for i in range(10,20):
time.sleep(1)
print('%d***%s' % (i, name))
lock.release()
if __name__ == '__main__':
lock=Lock()
p1=Process(target=demo,args=(lock,'p1',))
p2 = Process(target=demo2, args=(lock,'p2',))
p1.start()
p2.start()
但是使用锁并发就会变成串行,会牺牲程序效率,这样似乎就失去了多进程的意义,所以一般推荐用下面的队列。
进程间通信
1.Queue
queue就是队列,会开辟一个安全的全局队列,多进程在队列中操作数据,实现进程间通信
参数
- 有一个maxsize参数,用来限定队列中最大的元素个数,省略无大小限制
class Queue(object):
def __init__(self, maxsize=0, *, ctx):
if maxsize <= 0:
# Can raise ImportError (see issues #3770 and #23400)
from .synchronize import SEM_VALUE_MAX as maxsize
方法
- put:将obj压入队列,如果block为Ture,并且timeout为正,当队列满时,会阻塞timeout时间,等待队列中有有元素被弹出释放了队列空间,等到timeout时间队列依旧是满的就抛出Queue.Full异常,如果block为False,队列满时如果还有元素企图加入队列,会立即抛出Queue.Full异常
- get:从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
- empty:队列为空返回True,不安全,如果正在执行empty时put了一个元素
- full:队列满时返回True,也不安全,同上
- get_nowait
- put_nowait
def get_nowait(self):
return self.get(False)
def put_nowait(self, obj):
return self.put(obj, False)
- close:关闭队列阻止别的元素加入,会等待已近入队但未写入的元素执行完毕
继续是经典的生产者,消费者模式
from multiprocessing import Process,Queue
import time
import random
def Producer(q):
'''生产者类,超过20等待'''
while True:
if not q.full():
food=random.randint(1,100)
print('生产%d'%food)
q.put(food)
time.sleep(1)
else:
print('q is full')
def Consumer(q):
while True:
if not q.empty():
s=q.get()
print('消费:%d'%s)
time.sleep(2)
else:
print('q is empty')
if __name__ == '__main__':
q=Queue(20)
q1=Process(target=Producer,args=(q,))
q2=Process(target=Consumer,args=(q,))
q1.start()
q2.start()
2.管道
管道会返回一个元组,包含两个元素,就是管道的两个端,一个接收端,一个发送端,相当于在进程间创建了一个数据通路。
参数和方法
-
dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
-
close:关闭连接
-
filleno:返回连接使用的整数文件描述符
-
poll:如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。
-
recv:接受send发送的对象,没有要接受的对象会一直阻塞,如果另一端关闭,抛出EOFError
-
recv_bytes:接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。
-
recv_bytes_into:接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。
-
send:通过管道发送对象
-
send_bytes:通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收
# 管道
from multiprocessing import Process,Pipe
def demo1(pipe):
left,right=pipe
right.close()
while True:
try:
s=left.recv()
print(s)
except:
left.close()
print('pipe close')
break
def demo2(pipe):
left,right=pipe
left.close()
for i in range(10):
try:
right.send(i)
except:
print('pipe close')
break
right.close()
if __name__ == '__main__':
pipe=Pipe()
left, right = pipe
p1=Process(target=demo1,args=(pipe,))
p2=Process(target=demo2,args=(pipe,))
p1.start()
p2.start()
left.close()
right.close()