Python3进阶—多进程【全】

参考博文 https://www.cnblogs.com/jiangfan95/p/11439207.html

一、多进程模块multiprocessing

python中的多线程因为GIL的原因,无法利用多核优势,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程,这就要用到多进程模块multiprocessing。
常用的类、方法有:
multiprocessing.Process 用于创建子进程
multiprocessing.Queue 队列,用于进程间的数据同步、共享
multiprocessing.Pipe 管道,用于进程间的数据同步、共享
multiprocessing.Lock 互斥锁
multiprocessing.RLock 递归锁
multiprocessing.Value 用于进程间的数据同步、共享

1.1 multiprocessing.Process()

以下为Process的构造方法:

class Process(object):
    def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
        self.name = ''  
        self.daemon = False 
        self.authkey = None
        self.exitcode = None
        self.ident = 0
        self.pid = 0
        self.sentinel = None

从构造方法中可以看出,我们实例化一个Process进程对象时,可以填写的参数有五个:
group:该参数一般不填写,实际也没有用到
target:目标函数名,就是你在子进程中需要执行的函数,必填
name:进程名字,是字符串类型,非必填
args:target指定的函数中需要用到的参数,是一个元祖,非必填
kwargs:target指定的函数中需要用到的key-value形式的参数,是一个字典,非必填
使用multiprocess.Process()创建启动子进程,有两种方法。

方法一:直接调用Process()实例化一个进程对象

# 方法一 直接调用
import time
import random
from multiprocessing import Process


def do_something(name):
    print('%s runing' % name)
    time.sleep(random.randint(2, 5))
    print('%s running end' % name)


p1 = Process(target=do_something, args=('process-1',))  
# 这里要注意的是args这个参数是一个元祖,当只有一个元素时,记得加逗号
p2 = Process(target=do_something, args=('process-2',))
p3 = Process(target=do_something, args=('process-3',))
p4 = Process(target=do_something, args=('process-4',))

p1.start()
p2.start()
p3.start()
p4.start()
time.sleep(1)
print('主进程结束')

方法二:继承Process类,重写run方法

# 方法二:重写run方法
import time
import random
from multiprocessing import Process


class MyProcess(Process):
    def __init__(self, name):
        super(MyProcess, self).__init__()  # 调用父类的构造函数
        self.name = name

    def run(self) -> None:
        print('%s runing' % self.name)
        time.sleep(random.randint(2, 5))
        self.do_something()
        print('%s running end' % self.name)

    def do_something(self):
        pass


p1 = MyProcess('process-1')
# 这里要注意的是args这个参数是一个元祖,当只有一个元素时,记得加逗号
p2 = MyProcess('process-2')
p3 = MyProcess('process-3')
p4 = MyProcess('process-4')

p1.start()
p2.start()
p3.start()
p4.start()
time.sleep(1)
print('主进程结束')

1.2 join进程阻塞

有时我们需要等待创建的某个子进程执行完毕之后,再继续执行主进程的任务,这时候就需要用到join()这个方法。

def do_something(name):
    print('%s running' % name)
    if name == "process-2":
        time.sleep(4)
    time.sleep(1)
    print('%s running end' % name)


p1 = Process(target=do_something, args=('process-1',))
p2 = Process(target=do_something, args=('process-2',))
p1.start()
p2.start()
p2.join()  # 这里设置让p2进程执行完毕之后再继续执行主进程,注意 join方法需要在进程start之后才能使用
print('主进程结束')

1.3 daemon守护进程

daemon()这个方法用于设置进程为守护进程,守护进程会随着主进程结束而结束,不管它有没有执行完,具体看如下例子:

import time
from multiprocessing import Process


def do_something(name):
    print('%s running' % name)
    if name == "process-2":
        time.sleep(4)
    time.sleep(1)
    print('%s running end' % name)


p1 = Process(target=do_something, args=('process-1',))
p2 = Process(target=do_something, args=('process-2',))
p2.daemon = True  # 设置p2进程为守护进程,注意设置守护进程需要在进程start之前设置
p1.start()
p2.start()
time.sleep(1)
print('主进程结束')

output

process-1 running
process-2 running
process-1 running end
主进程结束

从输出的打印我们可以看出,p2进程还没有执行完毕,就随着主进程结束而结束了。

二、进程间同步

进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理。
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。

2.1 互斥锁Lock()

下面举一个模拟抢票的例子:

# 先在当前目录新建db.txt,文件的内容为:{"count":1}
# 注意一定要用双引号,不然json无法识别
# 购票行为由并发变成了串行,牺牲了运行效率,但保证了数据安全
from multiprocessing import Process,Lock
import time
import json


def search():
    """查询剩余票数"""
    dic = json.load(open('db.txt'))
    print('剩余票数%s' % dic['count'])


def get():
    """购票"""
    dic = json.load(open('db.txt'))
    time.sleep(0.1)  # 模拟读数据的网络延迟
    if dic['count'] > 0:
        dic['count'] -= 1
        time.sleep(0.1)  # 模拟写数据的网络延迟
        json.dump(dic, open('db.txt', 'w')) # 更新剩余票数
        print('购票成功' + "*" * 50)


def task(_lock):
    search()
    _lock.acquire()   # 获取锁
    get()
    _lock.release()   # 释放锁


if __name__ == '__main__':
    lock = Lock() # 互斥锁
    for i in range(10):  # 模拟并发100个客户端抢票
        p = Process(target=task, args=(lock,))
        p.start()

2.2 递归锁RLock()

递归锁,意思是一个进程如果已经获取了递归锁,那么它还可以再次获取,而不需要等待锁被释放。下面举一个简单的例子:

from multiprocessing import Process, RLock
# 初始化一个递归锁
lock = RLock()

def test_RLock(n):
    if n == 0:
        return
    lock.acquire()
    print("获取锁")
    test_RLock(n-1) # 进入递归,会再次调用lock.acquire()获取锁,如果这个锁是互斥锁,就会导致死锁。但是递归锁不会,它一旦被获取之后,可以在该进程内重复获取。
    lock.release()
    print("释放锁")

test_RLock(2)

三、进程间通信

与线程不同的是,因为每个进程都有自己独立的内存空间,多进程间没有任何共享状态,没办法直接通信,必须通过一定的方法进行进程间通信,除了用文件进行共享数据来达到进程间通信的目的之外,进程间通信的常见的方法还有队列、管道、信号量、共享内存、事件等。
文件共享数据实现进程间通信的缺点:
1)效率低(共享数据基于文件,而文件是硬盘上的数据)
2)需要自己加锁处理

因此我们最好找寻一种解决方案能够兼顾:
1)效率高(多个进程共享一块内存的数据)
2)帮我们处理好锁问题。

mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。

1 队列和管道都是将数据存放于内存中
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来, 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性

3.1 队列(推荐使用)

from multiprocessing import Queue
q = Queue(3)  # 定义一个容量为3的队列

q.put(1) 
q.put(2)
q.put(3)
print(q.full())  # 满了会返回True

print(q.get())  # 队列是先进先出,所以首先拿到的是1
print(q.get())
print(q.get())
print(q.empty())  # 空了会返回True

output:

True
1
2
3
True

3.2 队列的应用-生产者与消费者模式

什么是生产者消费者模式

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

from multiprocessing import Process, Queue
import time
import random
import os

def consumer(q):
    while True:
        res = q.get() 
        time.sleep(random.randint(1, 2))
        print('\033[45m%s 吃 %s\033[0m' % (os.getpid(),res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1, 2))
        res = '包子%s' % i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' % (os.getpid(), res))

if __name__ == '__main__':
    q = Queue()
    # 生产者们:即厨师们
    p1 = Process(target=producer, args=(q,))
    # 消费者们:即吃货们
    c1 = Process(target=consumer, args=(q,))
    # 开始
    p1.start()
    c1.start()
    print('主进程结束')

如果你执行了上述代码,你会发现主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c没有在队列获取到值,就会一直卡在q.get()这一步。

解决方式很简单,让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环。
我们接着上面那个通俗的例子,假设厨房和餐厅大堂是完全隔离的,只能通过一个小窗口进行传递包子,如果厨师每天规定只能生产100个包子,生产完之后就下班了。但是在大堂消费者并不知道厨师下班了,就一直在等着这个包子递出来,这样就会导致消费者进程一直阻塞。那么解决方式就是,厨师生产完全部包子之后,就往传递包子的小窗口里面挂一个牌子,通知消费者我已经下班了,不继续生产了,你们可以不用等了。

具体看如下代码:

from multiprocessing import Process,Queue
import time
import random
import os

def consumer(q):
    while True:
        res = q.get()
        # 收到结束信号None则结束
        if res is None:
            break 
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' % (os.getpid(), res))

def producer(q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res = '包子%s' % i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' % (os.getpid(), res))
    q.put(None)  # 发送结束信号None,或者其他约定的结束信号
       
if __name__ == '__main__':
    q = Queue()
    # 生产者们:即厨师们
    p1 = Process(target=producer,args=(q,))
    # 消费者们:即吃货们
    c1 = Process(target=consumer,args=(q,))
    # 开始
    p1.start()
    c1.start()
    print('主')

注意:结束信号None,不一定要由生产者发,主进程里同样可以发,但主进程需要等生产者结束后才应该发送该信号,我们可以给生产者进程加上join()方法。

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(q):
    for i in range(2):
        time.sleep(random.randint(1,3))
        res='包子%s' %i
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))

if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,))
    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    c1.start()

    p1.join() # 等待生产者进程结束
    q.put(None) # 发送结束信号
    print('主')

但上述解决方式,在有多个生产者和多个消费者时,应该怎么做呢?有几个消费者就发几次信号?没错,我们要保证通知到每一个消费者,以免它们一直阻塞。

from multiprocessing import Process,Queue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        if res is None:break #收到结束信号则结束
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))

def producer(name,q):
    for i in range(2):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,i)
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))


if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=('包子',q))
    p2=Process(target=producer,args=('骨头',q))
    p3=Process(target=producer,args=('泔水',q))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))

    #开始
    p1.start()
    p2.start()
    p3.start()
    c1.start()

    p1.join() #必须保证生产者全部生产完毕,才应该发送结束信号
    p2.join()
    p3.join()
    q.put(None) #有几个消费者就应该发送几次结束信号None
    q.put(None) #发送结束信号
    print('主')

有几个消费者就应该发送几次结束信号None。
其实我们的思路无非是发送结束信号而已,有另外一种队列提供了这种机制,JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

参数介绍:
maxsize是队列中允许最大项数,省略则无大小限制。

方法介绍:
JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:

q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常。

q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止。

from multiprocessing import Process,JoinableQueue
import time,random,os
def consumer(q):
    while True:
        res=q.get()
        time.sleep(random.randint(1,3))
        print('\033[45m%s 吃 %s\033[0m' %(os.getpid(),res))
        q.task_done() #向q.join()发送一次信号,证明一个数据已经被取走了

def producer(name,q):
    for i in range(10):
        time.sleep(random.randint(1,3))
        res='%s%s' %(name,i)
        q.put(res)
        print('\033[44m%s 生产了 %s\033[0m' %(os.getpid(),res))
    q.join()


if __name__ == '__main__':
    q=JoinableQueue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=('包子',q))
    p2=Process(target=producer,args=('骨头',q))
    p3=Process(target=producer,args=('泔水',q))

    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))
    c1.daemon=True
    c2.daemon=True

    #开始
    p_l=[p1,p2,p3,c1,c2]
    for p in p_l:
        p.start()

    p1.join()
    p2.join()
    p3.join()
    print('主') 
    
    #主进程等--->p1,p2,p3等---->c1,c2
    #p1,p2,p3结束了,证明c1,c2肯定全都收完了p1,p2,p3发到队列的数据
    #因而c1,c2也没有存在的价值了,应该随着主进程的结束而结束,所以设置成守护进程

3.3 管道

创建管道的类:
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道

参数介绍:
dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。

from multiprocessing import Process,Pipe

import time,os
def consumer(p,name):
    left,right=p
    left.close()
    while True:
        try:
            baozi=right.recv()
            print('%s 收到包子:%s' %(name,baozi))
        except EOFError:
            right.close()
            break
def producer(seq,p):
    left,right=p
    right.close()
    for i in seq:
        left.send(i)
        # time.sleep(1)
    else:
        left.close()
if __name__ == '__main__':
    left,right=Pipe()

    c1=Process(target=consumer,args=((left,right),'c1'))
    c1.start()


    seq=(i for i in range(10))
    producer(seq,(left,right))

    right.close()
    left.close()

    c1.join()
    print('主进程')

# 基于管道实现进程间通信(与队列的方式是类似的,队列就是管道加锁实现的)

注意:生产者和消费者都没有使用管道的某个端点,就应该将其关闭,如在生产者中关闭管道的右端,在消费者中关闭管道的左端。如果忘记执行这些步骤,程序可能再消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生产EOFError异常。因此在生产者中关闭管道不会有任何效果,付费消费者中也关闭了相同的管道端点。

管道可以用于双向通信,利用通常在客户端/服务器中使用的请求/响应模型或远程过程调用,就可以使用管道编写与进程交互的程序

from multiprocessing import Process,Pipe

import time,os
def adder(p,name):
    server,client=p
    client.close()
    while True:
        try:
            x,y=server.recv()
        except EOFError:
            server.close()
            break
        res=x+y
        server.send(res)
    print('server done')
if __name__ == '__main__':
    server,client=Pipe()

    c1=Process(target=adder,args=((server,client),'c1'))
    c1.start()

    server.close()

    client.send((10,20))
    print(client.recv())
    client.close()

    c1.join()
    print('主进程')
#注意:send()和recv()方法使用pickle模块对对象进行序列化。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值