python实现多进程(三)

本文继续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的区别:

  1. Pipe用来在2个进程间通信;Pipe()返回一对连接对象,代表了Pipe的2端。每个对象都有send()和recv()方法。
  2. 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()

运行结果:

你可能感兴趣的:(python实现多进程(三))