本文继续python多任务编程思想(一)和 python多任务编程思想(二)讨论python多进程话题,展开python多进程编程中的最后一个知识点,python进程间通信的方法。
进程间由于空间独立,资源无法互相直接获取,此时在不同的进程间进行数据传递就需要专门的通信方法。进程间通信的方法包含管、消息队列、共享内存、信号、信号量以及本地套接字。下面我们依次展开介绍。
一. 管道通信
在内存中开辟一段内存空间,形成管道结构。管道对多个进程可见,进程可以对管道进行读写操作。管道使用multiprocessing模块的Pipe类创建,Pipe类的使用总结如下:
from multiprocessing import Pipe
fd1, fd2 = Pipe(duplex=True)
功能:创建一个管道
参数:默认为双向管道;如果设置为False,则为单向管道
返回值:双向管道的fd1、fd2都可以进行读写操作;单向管道,fd1只可读,fd2只能写。
fd.recv()
功能:从管道读取内容,如果管道无内容,则阻塞;
fd.send()
功能:向管道写入内容;
参数:发送的内容; 在socket通信时,收发都只能是字节流的形式,管道几乎可以发送所有Python支持的数据,如:
fd.send("Hello")
fd2.send({'a':'Alex','b':'Bob'})
fd2.send([1,2,3,4,5])
fd2.send((name,))
from multiprocessing import Process,Pipe
import os,time
fd1,fd2 = Pipe(duplex=False) # 创建单向管道
def fun(name):
'''向管道内写入内容'''
time.sleep(3)
fd2.send({name})
print('子进程{}向管道中发送了'.format(os.getpid()), {name})
jobs = []
for i in range(3):
p = Process(target=fun,args=(i,))
jobs.append(p) # 用列表在创建时记录每个进程,便于子进程的回收
p.start()
print('主进程{}创建了子进程{}'.format(os.getpid(), p.pid))
for i in range(3):
data = fd1.recv() # 读取管道
print('主进程{}从管道中读取了'.format(os.getpid()), data)
for process in jobs:
print('主进程{}阻塞等待回收子进程{}'.format(os.getpid(), process.pid))
process.join() # 阻塞等待回收子进程
运行结果:3个子进程向管道中分别发送python集合,主进程从管道中按序获取所有数据
二. 消息队列
在内存中开辟队列结构空间,多个进程可以向队列投放消息,读取时遵循先进先出的原则;队列使用multiprocessing模块的Queue类创建,关于Queue类和方法的使用总结如下:
queue = Queue(maxsize=0)
功能:创建队列
参数:queue默认根据系统分配空间存储消息;指定maxsize,则表示最多存放多少条消息
返回值:队列对象
queue.put(data, [block, timeout])
功能:存放消息
参数:data表示存入的消息,可以是任意Python数据类型;block默认为True,表示当队列满的时候阻塞;当block为True,timeout表示超时时间
data = queue.get([block, timeout])
功能:取出消息
参数:block默认为True,当队列为空时阻塞;当block为True时,timeout表示超时时间
返回值:返回获取到的消息
queue.full()
判断队列是否为满。
queue.empty()
判断队列是否为空。
queue.size()
返回当前序列消息总数。
queue.close()
关闭队列。
from multiprocessing import Process,Queue
import time, os
q = Queue()
def fun1():
time.sleep(1)
print('子进程{}向队列发送'.format(os.getpid()), [1,2,3,4])
q.put([1,2,3,4])
print('子进程{}向队列发送'.format(os.getpid()), (5,6,7,8))
q.put((5,6,7,8))
print('子进程{}向队列发送'.format(os.getpid()), {9,0,1,2})
q.put({9,0,1,2})
def fun2():
print("子进程{}收到消息:".format(os.getpid()),q.get())
print("子进程{}收到消息:".format(os.getpid()),q.get())
print("子进程{}收到消息:".format(os.getpid()),q.get())
p1 = Process(target = fun1)
p2 = Process(target = fun2)
p1.start()
p2.start()
print('主进程{}创建了子进程{}和{}'.format(os.getpid(), p1.pid, p2.pid))
p1.join()
p2.join()
运行结果:子进程8437和8438使用Queue进行通信,8437使用put发送数据,8438使用get接收数据,顺序为先进先出。
Queue和Pipe的区别:
- Pipe用来在2个进程间通信;Pipe()返回一对连接对象,代表了Pipe的2端。每个对象都有send()和recv()方法。
- Queue用来在多个进程间实现通信;使用put()和get()方法向队列中写入和读取数据;
三. 共享内存
在内存中开辟一段空间,存储数据,对多个进程可见。每次写入共享内存中的数据会覆盖之前的内容。使用multiprocessing中的Value、Array创建共享内存,关于Value和Array的使用总结如下:
obj = Value(ctype, obj)
功能:开辟共享内存空间
参数:ctype为字符串类型,表示要转变的C的数据类型; obj共享内存的初始化数据;
返回值:共享内存对象
obj.value
表示共享内存中的值,可对其修改或者使用。
obj = Array(ctype, obj)
功能:开辟共享内存
参数:ctype为字符串类型,表示要转变的C的数据类型;obj为列表,则将列表存入共享内存,数据类型必须一致;obj为正整数,表示开辟几个数据空间;
'''Value,Array是python中共享内存映射文件的方法,速度比较快'''
import os, time
from multiprocessing import Process,Value,Array
def func(n,a):
n.value += 1;
for i in range(len(a)):
a[i] *= 2
if __name__ == "__main__":
num = Value("i",1) # i代表C语言中的整型,1为写入共享内存的数据
arr = Array("i",range(10))
print('主进程{}开辟共享内存num和arr,初始值分别为{}, {}'.format(os.getpid(), num.value, arr[::]))
p = Process(target=func,args=(num,arr))
p.start()
time.sleep(2)
print('子进程{}将共享内存num和arr中的值分别修改成了{}, {}'.format(p.pid, num.value, arr[::]))
p.join()
运行结果:
四. 信号
4.1 信号的基本概念
一个进程向另一个进程通过信号传递某种讯息,接收方在接收到信号后进行相应处理,如采取终止进程,暂停进程,忽略产生等行为。在Linux终端输入“kill -l”命令可查看操作系统的信号名称和对应的编号,如下:
使用 kill -signum PID
命令可以向指定进程发送一个信号,比如我们经常使用的“kill -9 进程号 ” 去杀死一个进程,即向某个该进程发送了SIGKILL信号。常用信号的含义总结如下:
信号名称 | 信号的默认行为 |
---|---|
SIGHUP | 终端断开 |
SIGINT | ctrl + c |
SIGQUIT | ctrl + \ |
SIGTSTP | ctrl + z |
SIGKILL | 终止进程且不能被处理 |
SIGSTOP | 暂停进程且不能被处理 |
SIGALRM | 时钟信号 |
SIGCHLD | 子进程状态改变发给父进程 |
4.2 使用python进行信号处理
在进一步学习信号前,我们先来区分下同步/异步的概念:
- 同步是指按照步骤一步一步顺序执行,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行。
- 异步是和同步相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接去处理后续的事物,通过状态、通知、回调来通知调用者处理结果。
在使用“信号”进行通信时,程序利用内核,不影响应用层程序持续执行。信号是唯一的异步通信方式。python使用os模块和signal模块操作信号,常用方法总结如下:
os.kill(pid, sig)
功能:发送信号给某个进程
参数:pid指定给哪个进程发送信号;sig指定具体发送什么信号
signal.alarm(sec)
功能:指定一定时间后给自身发送一个 SIGALRM信号
参数:指定时间
注意: 一个进程中只能设置一个时钟,第二个时钟会覆盖之前的时间
signal.pause()
阻塞等待一个信号的发生。
signal.signal(signum, handler)
功能:处理信号
参数:signum:要处理的信号;handler:信号的处理方法;SIG_DFL表示使用默认的方法处理,SIG_IGN则忽略这个信号,此外handler还可传入一个自定义函数处理信号;
注意:
- signal函数是一个异步处理函数
- signal函数不能处理SIGKILL、SIGSTOP信号
- 在父进程中使用signal(SIGCHLD, SIG_IGN)处理子进程退出信号;这样子进程退出时会交给系统处理;是解决僵尸进程的惯用手法!
import signal
from time import sleep
signal.alarm(5) # 设置一个闹钟
signal.signal(signal.SIGINT, signal.SIG_IGN) # 忽略SIGINT ctrl + c
signal.signal(signal.SIGTSTP, signal.SIG_DFL) # 默认方法处理SIGTSTP ctrl + z
def handler(sig,frame):
'''信号处理函数:定义函数处理SIGQUIT和SIGALARM'''
if sig == signal.SIGALRM:
print("收到时钟信号,终止无效!")
elif sig == signal.SIGQUIT:
print("ctrl + \, 退出无效!")
signal.signal(signal.SIGALRM, handler)
signal.signal(signal.SIGQUIT, handler)
while True:
print("waiting for signal")
sleep(2)
运行结果:5s后程序收到一个时钟信号,调用handler函数处理该信号;当在终端按下"Ctrl + c",程序收到系统发送的SIGINT信号,并忽略该信号,因此程序并不中断执行;当在终端按下"Ctrl + ",程序收到SIGQUIT信号,调用handler处理该信号;当在终端按下"Ctrl + z",程序收到SIGTSTP信号,使用该信号的默认处理方式处理该信号,程序最终退出。
五. 信号量
信号量是指给定一定的数量,对多个进程可见,并且多个进程根据信号量的多少确定不同的行为(可用于操作共享的有限资源)。信号量的实现使用multiprocessing提供的Semaphore实现,类和方法的使用总结如下:
semaphore = Semaphore(num)
功能:生成信号量对象。
参数:num表示信号量的初始值。
返回值:信号量对象。
semaphore .acquire()
信号量数量减1,信号量为0时该方法的调用将阻塞。
semaphore.release()
信号量数量加1。
semaphore.get_value()
获取当前信号量的值。
import os
from time import sleep
from multiprocessing import Semaphore,Process
sem = Semaphore(2) #创建信号量对象,并设置信号量的初始值为3
def fun():
sem.acquire()
print("进程{}消耗了信号量,当前信号量是{}".format(os.getpid(), sem.get_value()))
sleep(3)
sem.release()
print("进程{}释放了了信号量,当前信号量是{}".format(os.getpid(), sem.get_value()))
jobs = []
for i in range(3):
p = Process(target = fun)
jobs.append(p)
p.start()
print("在主进程中创建了子进程:", [process.pid for process in jobs])
for i in jobs:
i.join()
运行结果:创建的信号量初始值为2,当12042和12041进程消耗信号量至0后,12043进程必须阻塞等待有新的信号量被释放。
六. 本地套接字
linux进程间通信还可以采用socket本地套接字,socket函数的第一个参数设置为socket.AF_UNIX表示创建本地套接字;使用方法类似于socket网络编程。此处不再赘述。
七. 同步互斥机制
多进程编程中,为了解决多个进程对对共有资源操作产生的争夺,必须使用同步互斥机制。其中同步是一种合作关系,为完成某个任务,多进程或者多线程之间形成一种协调。按照约定依次执行对临界资源的操作,相互告知相互促进。互斥则是一种制约关系,当一个进程占有临界资源就会进行加锁的操作,此时其他进程就无法操作该临界资源。直到使用的进程进行解锁操作后才能使用。
python中使用multiprocessing模块提供的Event和Lock类实现同步互斥,关于二者的常用方法总结如下:
类/方法 | 说明 |
---|---|
event = multiprocessing.Event() | 创建事件对象 |
event.wait([timeout]) | 事件阻塞 |
event.set() | 当event被set后,event.wait()不再阻塞 |
event.clear() | 当event被清楚后,event.wait()又会阻塞 |
event.is_set() | 判断当前事件对象是否被设置 |
lock = multiprocessing .Lock() | 创建锁对象 |
lock.acquire() | 上锁:上锁状态执行acquire()操作会阻塞 |
lock.release() | 解锁:解锁状态执行acquire()不阻塞 |
示例代码1 使用Event实现同步互斥,要求:
- 三个进程都要操作共享资源;
- 要求必须主进程先操作;
- 子进程中谁先操作都可以,但是有一个子进程不能长期阻塞;
import os
from multiprocessing import Process,Event
from time import sleep, ctime
def wait_event():
print("子进程{}等待父进程完成对共享资源的操作...".format(os.getpid()))
e.wait()
print("子进程{}结束阻塞,此时event.is_set()返回值为{}".format(os.getpid(), e.is_set()))
def wait_event_timeout(sec):
print(ctime(), "子进程{}只等父进程{}s".format(os.getpid(), sec))
e.wait(sec)
print(ctime(), "子进程{}结束阻塞,此时event.is_set()返回值为{}".format(os.getpid(), e.is_set()))
e = Event()
p1 = Process(target = wait_event)
p2 = Process(target = wait_event_timeout, args = (2,))
p1.start()
p2.start()
print("主进程要先操作资源")
sleep(3)
print("主进程操作完毕,set")
e.set()
p1.join()
p2.join()
运行结果:event.wait()默认阻塞,只有在event.set()执行之后或者超时,才会结束阻塞。由超时导致的阻塞结束,is_set()返回值仍为False。
示例代码2 使用Lock实现同步互斥操作(注:sys.stdout 为共享资源,所有进程都可以操作)
import sys
from multiprocessing import Process,Lock
def writer1():
lock.acquire()
for i in range(3):
sys.stdout.write("人生苦短\n")
lock.release()
def writer2():
with lock: # Lock实现了上下文管理器
for i in range(3):
sys.stdout.write("我用Python\n")
lock = Lock()
w1 = Process(target = writer1)
w2 = Process(target = writer2)
w1.start()
w2.start()
w1.join()
w2.join()
运行结果: