并发编程——多进程

时间:2021-09-12 16:43:11

---恢复内容开始---

本节导读:

  • multiprocessing模块介绍
  • process类的介绍
  • 开启进程的两种方式
  • join方法
  • 守护进程
  • 互斥锁
  • 队列
  • 生产者与消费者模型

 

一 multiprocessing模块介绍

  python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu\_count\(\)查看),在python中大部分情况需要使用多进程。

Python提供了multiprocessing。 multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,>提供了Process、Queue、Pipe、Lock等组件。

需要再次强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。

二 Process类的介绍

创建进程

Process([group [, target [, name [, args [, kwargs]]]]])
#由该类实例化得到的对象,可用来开启一个子进程

强调:
1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

参数介绍

#group参数未使用,值始终为None
#target表示调用对象,即子进程要执行的任务
#args表示调用对象的位置参数元组,args=(1,2,'egon',)
#kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}
#name为子进程的名称

方法介绍

p.start()  #启动进程,并调用该子进程中的p.run() 
p.run() #进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
p.terminate() #强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
p.is_alive() #如果p仍然运行,返回True
p.join([timeout]) #主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间。

属性介绍

p.daemon  #默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
p.name #进程的名称
p.pid # 进程的pid

 三 开启进程的两种方式

注意:在windows中Process()必须放到# if __name__ == '__main__':下

方法一

并发编程——多进程并发编程——多进程
import time
import random
from multiprocessing import Process

def piao(name):
    print('%s piaoing' %name)
    time.sleep(random.randrange(1,5))
    print('%s piao end' %name)

if __name__ == '__main__':
    #实例化得到四个对象
    p1=Process(target=piao,args=('egon',)) #必须加,号
    p2=Process(target=piao,args=('alex',))
    p3=Process(target=piao,args=('wupeqi',))
    p4=Process(target=piao,args=('yuanhao',))

    #调用对象下的方法,开启四个进程
    p1.start()
    p2.start()
    p3.start()
    p4.start()
    print('')
直接通过process实例化

 方法二

并发编程——多进程并发编程——多进程
import time
import random
from multiprocessing import Process

class Piao(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        print('%s piaoing' %self.name)

        time.sleep(random.randrange(1,5))
        print('%s piao end' %self.name)

if __name__ == '__main__':
    #实例化得到四个对象
    p1=Piao('egon')
    p2=Piao('alex')
    p3=Piao('wupeiqi')
    p4=Piao('yuanhao')

    #调用对象下的方法,开启四个进程
    p1.start() #start会自动调用run
    p2.start()
    p3.start()
    p4.start()
    print('')
继承process类

四 join方法

在主进程运行过程中如果想并发地执行其他的任务,我们可以开启子进程,此时主进程的任务与子进程的任务分两种情况

情况一:在主进程的任务与子进程的任务彼此独立的情况下,主进程的任务先执行完毕后,主进程还需要等待子进程执行完毕,然后统一回收资源。

情况二:如果主进程的任务在执行到某一个阶段时,需要等待子进程执行完毕后才能继续执行,就需要有一种机制能够让主进程检测子进程是否运行完毕,在子进程执行完毕后才继续执行,否则一直在原地阻塞,这就是join方法的作用

并发编程——多进程并发编程——多进程
from multiprocessing import Process
import time
import random
import os

def task():
    print('%s is piaoing' %os.getpid())
    time.sleep(random.randrange(1,3))
    print('%s is piao end' %os.getpid())

if __name__ == '__main__':
    p=Process(target=task)
    p.start()
    p.join() #等待p停止,才执行下一行代码
    print('')
join

 

五 守护进程

   如果我们有两个任务需要并发执行,那么开一个主进程和一个子进程分别去执行就ok了,如果子进程的任务在主进程任务结束后就没有存在的必要了,那么该子进程应该在开启前就被设置成守护进程。主进程代码运行结束,守护进程随即终止

关于守护进程需要强调两点:

其一:守护进程会在主进程代码执行结束后就终止

 

其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

开启守护进程方法:

并发编程——多进程并发编程——多进程
from multiprocessing import Process
import time
import random

def task(name):
    print('%s is piaoing' %name)
    time.sleep(random.randrange(1,3))
    print('%s is piao end' %name)


if __name__ == '__main__':
    p=Process(target=task,args=('egon',))
    p.daemon=True #一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行
    p.start()
    print('') #只要终端打印出这一行内容,那么守护进程p也就跟着结束掉了
Daemon

六 互斥锁

  进程之间数据隔离,但是共享一套文件系统,因而可以通过文件来实现进程直接的通信,而多个进程同时操作同一数据,必然会导致数据错乱,这时候就要通过加互斥锁来解决。

并发编程——多进程并发编程——多进程
#由并发变成了串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process,Lock
import os,time
def work(lock):
    lock.acquire() #加锁
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())
    lock.release() #释放锁
if __name__ == '__main__':
    lock=Lock()
    for i in range(3):
        p=Process(target=work,args=(lock,))
        p.start()
lock

 总结:

加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行地修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。

虽然可以用文件共享数据实现进程间通信,但问题是:

1、效率低(共享数据基于文件,而文件是硬盘上的数据)

2、需要自己加锁处理

因此我们最好找寻一种解决方案能够兼顾:

1、效率高(多个进程共享一块内存的数据)

2、帮我们处理好锁问题。

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

队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,因而队列才是进程间通信的最佳选择。

我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

 

七 队列

进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的

队列的创建

 

from multiprocessing import Queue
q =Queue([maxsize])
#创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

 

参数介绍

maxsize是队列中允许最大项数,省略则无大小限制。
但需要明确:
    1、队列内存放的是消息而非大数据
    2、队列占用的是内存空间,因而maxsize即便是无大小限制也受限于内存大小

队列的使用

并发编程——多进程并发编程——多进程
from multiprocessing import Process,Queue

q=Queue(3)

#put ,get ,put_nowait,get_nowait,full,empty
q.put(1)   #用以插入数据到队列中。
q.put(2)
q.put(3)
print(q.full()) #满了
# q.put(4) #再放就阻塞住了

print(q.get())     #可以从队列读取并且删除一个元素。

print(q.get())
print(q.get())
print(q.empty()) #空了
# print(q.get()) #再取就阻塞住了
队列使用

 

八 生产者与消费者模型

为什么要使用生产者消费者模型

生产者指的是生产数据的任务,消费者指的是处理数据的任务,在并发编程中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

什么是生产者和消费者模式

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

这个阻塞队列就是用来给生产者和消费者解耦的

Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

参数介绍:

maxsize是队列中允许最大项数,省略则无大小限制。
但需要明确:
    1、队列内存放的是消息而非大数据
    2、队列占用的是内存空间,因而maxsize即便是无大小限制也受限于内存大小

主要方法介绍:

q.put方法用以插入数据到队列中。
q.get方法可以从队列读取并且删除一个元素。

队列的使用

from multiprocessing import Process,Queue

q=Queue(3)

#put ,get ,put_nowait,get_nowait,full,empty
q.put(1)
q.put(2)
q.put(3)
print(q.full()) #满了
# q.put(4) #再放就阻塞住了

print(q.get())
print(q.get())
print(q.get())
print(q.empty()) #空了
# print(q.get()) #再取就阻塞住了