8. 进程间通信

Process之间肯定需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue/Pipes等多种方式来交换数据。

(1) 消息队列Queue

简单的理解Queue实现进程间通信的方式,就是使用了操作系统给开辟的一个队列空间,各进程可以把数据放到该队列中,当然也可以从队列中把自己需要的信息取走。

Queue/queue模块常用属性

属性

描述

****Queue/queue模块的类****

Queue(maxsize=0)

创建一个先入先出队列。如果给定最大值,这在队列没有空间阻塞;否则为无限队列

LifoQueue(maxsize=0)

创建一个后入先出队列。如果给定最大值,这在队列没有空间阻塞;否则为无限队列

PriorityQueue(maxsize=0)

创建一个优先级队列。如果给定最大值,这在队列没有空间阻塞;否则为无限队列

****Queue/queue异常****

Empty

当对空队列调用get*()方法时抛出异常

Full

当对已满的队列调用put*()方法时抛出异常

****Queue/queue对象方法****

qsize()

返回队列大小(由于返回时队列大小可能被其他线程修改,所以该值为近似值)

empty()

如果队列为空,则返回True;否则返回False

full()

如果队列已满,则返回True;否则返回False

put(obj[, block=True[, timeout=None]])

将obj放入队列,如果block参数为True时,一旦队列被写满,则代码就会被阻塞,知道有进程取走数据并腾出空间供obj使用;timeout参数用来设置阻塞的时间,即程序最多在阻塞timeout秒之后,如果还是没有空闲的空间,程序就会抛出queue.Full异常

put_nowait(obj)

该方法等价于put(obj, False)

get([block=True[, timeout=None]])

从队列中取数据并返回,当block参数为True且timeout为None时,该方法会阻塞当前进程,知道队列中有可用的数据。如果block为False,则进程会直接做取数据的操作,如果取数据失败,则抛出queue.Empty异常(这种情形下timeout参数将不起作用)。如果设置timeout秒数,则当前进程最多被阻塞timeout秒,如果到时依旧没有可用的数据取出,则会抛出queue.Empty异常。

get_nowait()

该方法等价于get(False)

task_done()

用于表示队列中的某个元素已执行完成,该放方法会被下面的join()使用

join()

在队列中所有元素执行完毕并调用上面的task_done()信号钱,保持阻塞

# 进程中消息队列读写数据
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对象可调用的方法

属性

描述

send(obj)

发送一个obj给管道的另一端,另一端使用recv()方法就收。该obj必须是可序列化的对象,如果该对象序列化后超过32MB,则很可能会引发ValueError异常。

recv()

接受另一端通过send()方法发送过来的数据。

close()

关闭连接。

poll([timeout])

返回连接中是否还有数据可以读取。

send_bytes(buffer[, offset[, size]])

发送直接数据,如果 没有指定offset、size参数,则默认发送buffer字节串的全部数据;如果指定offset和size参数,则发送buffer字节串中从offset开始、长度为size的直接数据;通过该方法发送的数据,应该使用recv_bytes()recv_bytes_info()方法接收。

recv_bytes([maxlength])

接收通过send_bytes()发送的数据,maxlength指定最多接收的字节数,该方法返回接收到的直接数据。

recv_bytes_info(buffer[, offset])

功能类似于recv_bytes()方法类似,只是该方法将接收到的数据放在buffer中。

# 使用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操作系统的标准化协议)实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源。

信号分为可靠信号和不可靠信号,实时信号和非实时信号。

进程有三种方式响应信号: 忽略信号、捕捉信号、执行默认操作。