8. 进程间通信
Process之间肯定需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue/Pipes等多种方式来交换数据。
(1) 消息队列Queue
简单的理解Queue实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间,各进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走。
Queue/queue模块常用属性
属性 | 描述 |
****Queue/queue模块的类**** | |
| 创建一个先入先出队列。如果给定最大值,这在队列没有空间阻塞;否则为无限队列 |
| 创建一个后入先出队列。如果给定最大值,这在队列没有空间阻塞;否则为无限队列 |
| 创建一个优先级队列。如果给定最大值,这在队列没有空间阻塞;否则为无限队列 |
****Queue/queue异常**** | |
| 当对空队列调用 |
| 当对已满的队列调用 |
****Queue/queue对象方法**** | |
| 返回队列大小(由于返回时队列大小可能被其他线程修改,所以该值为近似值) |
| 如果队列为空,则返回True;否则返回False |
| 如果队列已满,则返回True;否则返回False |
| 将obj放入队列,如果block参数为True时,一旦队列被写满,则代码就会被阻塞,知道有进程取走数据并腾出空间供obj使用;timeout参数用来设置阻塞的时间,即程序最多在阻塞timeout秒之后,如果还是没有空闲的空间,程序就会抛出 |
| 该方法等价于 |
| 从队列中取数据并返回,当block参数为True且timeout为None时,该方法会阻塞当前进程,知道队列中有可用的数据。如果block为False,则进程会直接做取数据的操作,如果取数据失败,则抛出 |
| 该方法等价于 |
| 用于表示队列中的某个元素已执行完成,该放方法会被下面的 |
| 在队列中所有元素执行完毕并调用上面的 |
# 进程中消息队列读写数据
from multiprocessing import Process, Queue
import os
import time
import random
# 写数据
def write(q):
print(f"写进程:{os.getpid()}")
for i in 'ABC':
print(f'正在往消息队列写入{i}')
q.put(i)
time.sleep(random.random())
# 读数据
def reader(q):
while True:
if not q.empty():
i = q.get()
print(f"从消息队列中读出{i}")
time.sleep(random.random())
else:
break
if __name__ == "__main__":
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
pw.start() # 启动子进程pw,写入
pr.start() # 启动子进程pr,读取
pw.join() # 等待pw结束
pr.terminate() # pr进程里是死循环,无法等待期结束,只能强行终止
# 结果
'''
写进程:3808
正在往消息队列写入A
从消息队列中读出A
正在往消息队列写入B
从消息队列中读出B
正在往消息队列写入C
'''
(2) 管道Pipe
通常情况下,管道有两个口,而Pipe也常常用来实现两个进程间的通信,这两个进程分别位于管道的两端,一端用来发送数据,一段用来接收数据。使用Pipe实现进程通信,首先调用
multiprocessing.Pipe()
函数来创建一个管道。该函数的语法格式如下:
conn1, conn2 = multiprocessing.Pipe([duplex=True])
,其中conn1和conn2分别用来接收Pipe函数返回的两个端口;duplex参数默认为True,表示该管道是双向的,即位于两个端口的进程既可以发送数据也可以接收数据,若duplex=False
,则表示管道是单通道,conn1只能用来接收数据,而conn2只能用来发送数据。
Pipe对象可调用的方法
属性 | 描述 |
| 发送一个obj给管道的另一端,另一端使用 |
| 接受另一端通过 |
| 关闭连接。 |
| 返回连接中是否还有数据可以读取。 |
| 发送直接数据,如果 没有指定offset、size参数,则默认发送buffer字节串的全部数据;如果指定offset和size参数,则发送buffer字节串中从offset开始、长度为size的直接数据;通过该方法发送的数据,应该使用 |
| 接收通过 |
| 功能类似于 |
# 使用Pipe管道实现2个进程之间通信
import multiprocessing
def processFun(conn, name):
print(multiprocessing.current_process().pid, "进程发送数据:", name)
conn.send(name)
if __name__ == '__main__':
# 创建管道
conn1, conn2 = multiprocessing.Pipe()
# 创建子进程
p = multiprocessing.Process(target=processFun, args=(conn1, "http://www.baidu.com"))
# 启动子进程
p.start()
p.join()
print(multiprocessing.current_process().pid, "接收数据:")
print(conn2.recv())
# 结果
'''
5760 进程发送数据: http://www.baidu.com
7760 接收数据:
http://www.baidu.com
'''
(3) 共享内存
共享内存是一种常用的,高效的进程之间的通信方式,为了保证共享内存的有序访问,需要对进程采取额外的同步措施。
from multiprocessing import Process
import mmap
import contextlib
import time
def write():
with contextlib.closing(mmap.mmap(-1, 1024, tagname='cnblogs', access=mmap.ACCESS_WRITE)) as mem:
for share_data in ("Hello", "Alpha_Panda"):
mem.seek(0)
print('Write data:== %s == to share memory!' % share_data)
mem.write(str.encode(share_data))
mem.flush()
time.sleep(0.5)
def read():
while True:
invalid_byte, empty_byte = str.encode('\x00'), str.encode('')
with contextlib.closing(mmap.mmap(-1, 1024, tagname='cnblogs', access=mmap.ACCESS_READ)) as mem:
share_data = mem.read(1024).replace(invalid_byte, empty_byte)
if not share_data:
# 当共享内存没有有效数据时结束read
break
print("Get data:== %s == from share memory!" % share_data.decode())
time.sleep(0.5)
if __name__ == '__main__':
pr = Process(target=read, args=())
pw = Process(target=write, args=())
pw.start()
pr.start()
pw.join()
pr.join()
# 结果
'''
Write data:== Hello == to share memory!
Get data:== Hello == from share memory!
Write data:== Alpha_Panda == to share memory!
Get data:== Alpha_Panda == from share memory!
'''
(4) 信号量
通信原理:给定一个数量对多个进程可见,多个进程都可以操作该数量增减,并根据数量值决定自己的行为。
# 实现方法:
"""
from multiprocessing import Semaphore
sem = Semaphore(num)
功能 : 创建信号量对象
参数 : 信号量的初始值
返回值 : 信号量对象
sem.acquire() 将信号量减1 当信号量为0时阻塞
sem.release() 将信号量加1
sem.get_value() 获取信号量数量
"""
from multiprocessing import Process, Semaphore
import os
import time
# 创建信号量
sem = Semaphore(3) # 3表示服务程序最多允许三个进程同时执行事件
def foo():
print(f"进程 {os.getpid()} 想执行事件")
# 获取信号量
sem.acquire() # 减少信号量
print(f"进程 {os.getpid()} 开始执行操作")
start = time.time()
time.sleep(3)
end = time.time()
print(f"睡眠了{end - start: .4f}秒")
print(f"进程 {os.getpid()} 操作执行完毕")
sem.release() # 增加信号量
if __name__ == "__main__":
p = Process(target=foo, args='')
p.start()
p.join()
# 结果
'''
进程 4856 想执行事件
进程 4856 开始执行操作
睡眠了 3.0010秒
进程 4856 操作执行完毕
'''
(5) socket套接字
套接字无疑是通信使用最为广泛的方式了,socket不仅可以跨主机进行通信,甚至有时候可以使用socket在同一主机的不同进程间进行通信。
socket的语法:
socket = socket.socket(family, type[, protocal])
,family代表地址家族,一般为AF_UNIX,AF_INET和AF_INET6;AF_UNIX用于同一台机器上的进程通信,AF_INET用于IPV4协议的TCP/UDP,AF_INET6用于IPV6协议的TCP/UDP。type表示套接字类型,一般为SOCK_STREAM,SOCK_DGRAM和SOCK_RAM;SOCK_STREAM为流式套接字,用于TCP通信,SOCK_DGRAM为数据报式套接字,用于UDP通信,SOCK_RAM为原始套接字,可以用于处理ICMP/IGMP等网络报文,这是普通套接字无法处理的。protocal代表协议编号,默认为0。
# 服务端
import socket
# 1、创建服务端的socket对象
sk = socket.socket()
# 2、绑定一个ip和端口
sk.bind(("127.0.0.1", 8888))
# 3、服务器端一直监听是否有客户端进行连接
sk.listen(5)
while True:
# 4、如果有客户端进行连接、则接受客户端的连接
conn, addr = sk.accept() # 返回客户端socket通信对象和客户端的ip
# 5、客户端与服务端进行通信
rev_data = conn.recv(1024)
print('服务端收到客户端发来的消息:%s' % (rev_data.decode('GB2312')))
# 6、服务端给客户端回消息
conn.send(b"HTTP/1.1 200 OK \r\n\r\n") # http协议
show_str = "<h1> 这短短的一生,我们最终都会失去,你不妨大胆一些。爱一个人,攀一座山,追一个梦,加油 !!!</h1>"
conn.send(show_str.encode('GB2312'))
# 7、关闭socket对象
conn.close()
客户端可以自己写,也可以直接通过浏览器访问:
http://127.0.0.1:8888
# 客户端
import socket
# 1、创建socket通信对象
clientSocket = socket.socket()
# 2、使用正确的ip和端口去链接服务器
clientSocket.connect(("127.0.0.1", 8888))
# 3、客户端与服务器进行通信
# 给socket服务器发送信息
send_data = "你拼命赚钱的样子虽然有些狼狈。但是自己靠自己的样子真的很美!加油"
clientSocket.send(send_data.encode("GB2312"))
# 接收服务器的响应(服务器回复的消息)
recvData = clientSocket.recv(1024).decode("GB2312")
print("客户端收到服务器恢复的消息:%s" % (recvData))
# 4、关闭socket对象
clientSocket.close()
(6) 信号signal
参考链接
信号是在软件层次上对中断机制的一种模拟(是由系统内核 发出,由于错误内存冲突等原因引起产生的),在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX(一个针对Unix操作系统的标准化协议)实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源。
信号分为可靠信号和不可靠信号,实时信号和非实时信号。
进程有三种方式响应信号: 忽略信号、捕捉信号、执行默认操作。