并发编程之多进程

并发编程之多进程

一. 操作系统

1.1 什么是操作系统

操作系统就是一个协调、管理和控制计算机硬件资源与软件资源的控制程序。操作系统位于计算机硬件与应用软件之间,本质也是一个软件。操作系统由操作系统的内核(运行于内核态,管理硬件资源)以及系统调用(运行于用户态,为应用程序员写的应用程序提供系统调用接口)两部分组成,所以,单纯的说操作系统是运行于内核态的,是不准确的。

1.2 为什么要有操作系统

现代的计算机系统主要是由一个或者多个处理器,主存,硬盘,键盘,鼠标,显示器,打印机,网络接口及其他输入输出设备组成。

一般而言,现代计算机系统是一个复杂的系统。

其一:如果每位应用程序员都必须掌握该系统所有的细节,那就不可能再编写代码了(严重影响了程序员的开发效率:全部掌握这些细节可能需要一万年....)

其二:并且管理这些部件并加以优化使用,是一件极富挑战性的工作,于是,计算安装了一层软件(系统软件),称为操作系统。它的任务就是为用户程序提供一个更好、更简单、更清晰的计算机模型,并管理刚才提到的所有设备。

总结:

程序员无法把所有的硬件操作细节都了解到,管理这些硬件并且加以优化使用是非常繁琐的工作,这个繁琐的工作就是操作系统来干的,有了他,程序员就从这些繁琐的工作中解脱了出来,只需要考虑自己的应用软件的编写就可以了,应用软件直接使用操作系统提供的功能来间接使用硬件。

1.3 必备的理论知识

  1. 操作系统的作用:

    1. 隐藏丑陋复杂的硬件接口,提供良好的抽象接口

    2. 管理、调度进程,并且将多个进程对硬件的竞争变得有序

  2. 多道技术:

    1. 产生背景:针对单核,实现并发

    ps:现在的主机一般是多核,那么每个核都会利用多道技术。有4个cpu,运行于cpu1的某个程序遇到io阻塞,会等到io结束再重新调度,会被调度到4个cpu中的任意一个,具体由操作系统调度算法决定。

    1. 空间上的复用:如内存中同时有多道程序

    2. 时间上的复用:复用一个cpu的时间片

    强调:遇到io切换,占用cpu时间过长也切换,核心在于切之前将进程的状态保存下来,这样才能保证下次切换回来时,能基于上次切走的位置继续运行。

二. 进程

2.1 什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

2.2 进程的概念

进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

2.3 进程的特征

  • 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。

  • 并发性:任何进程都可以同其他进程一起并发执行

  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;

  • 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进

  • 结构特征:进程由程序、数据和进程控制块三部分组成。

多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

2.4 进程与程序的区别

程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态的概念。

程序可以作为一种软件资料长期存在,而进程是有一定生命期的。

程序是永久的,进程是暂时的。

注意:同一个程序执行两次,就会在操作系统中出现两个进程,所以我们可以同时运行一个软件,分别做不同的事情也不会混乱。

三. 进程调度

1、先来先服务调度算法

2、短作业优先调度算法

3、时间片轮转法

4、多级反馈队列算法

###多级反馈队列算法
1. 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
2. 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
3. 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

四. 进程的并行与并发

并发:指资源有限的情况下,交替轮流使用资源。

并行:指同时执行

区别:并行是真正意义上的同时执行;而并发只是看起来像同时执行的,但是是交替执行

img

五. 同步/异步和阻塞/非阻塞

5.1 状态介绍

进程三态状态装换图

167-同步异步阻塞非阻塞-01.png

在程序运行的过程中,由于被操作系统的调度算法控制,程序会进入几个状态:就绪,运行和阻塞。

  1. 就绪(Ready)状态:当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。

  2. 执行/运行(Running)状态当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。

  3. 阻塞(Blocked)状态正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

5.2 同步和异步

所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。

所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

5.3 阻塞和非阻塞

阻塞和非阻塞这两个概念与程序(线程)等待消息通知(无所谓同步或者异步)时的状态有关。也就是说阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。

5.4 同步/异步和阻塞/非阻塞

1、同步阻塞形式----------》效率最低

2、同步非阻塞形式

3、异步阻塞形式

4、异步非阻塞形式----------》效率最高

六. 开启多进程

6.1 multiprocessing模块

multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数)

功能:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内

6.2 Process类的介绍

创建进程的类

Process([group [, target [, name [, args [, kwargs]]]]])  #由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)

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

参数介绍

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

方法介绍

1 p.start()启动进程,并调用该子进程中的p.run() 

2 p.run():  进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  

3 p.terminate():  强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
   
4 p.is_alive():  如果p仍然运行,返回True

5 p.join([timeout]):  主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,# p.join只能join住start开启的进程,而不能join住run开启的进程

属性介绍

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

2 p.name:  进程的名称

3 p.pid: 进程的pid

4 p.exitcode:  进程在运行时为None、如果为–N,表示被信号N结束(了解即可)

5 p.authkey:  进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)

6.3 使用process模块创建进程

6.3.1 启动第一个子进程

import time
from multiprocessing import Process

def f(name):
   print('hello', name)
   print('我是子进程')
   
if __name__ == '__main__':
   p = Process(target=f, arg=('tom',))
   p.start()
   time.sleep(1)
   print('执行主进程')

6.3.2 join方法

import time
from multiprocessing import Process

def f(name):
   print('hello', name)
   time.sleep(1)
   print('我是子进程')
   
if __name__ == '__main__':
p = Process(target=f, arg=('tom',))
   p.start()
   p.join()
   print('执行主进程')

6.3.3 查看主进程和子进程的进程号

import os
from multiprocessing import Process

def f(x):
   print('子进程id:', os.getpid(),'父进程id:', os.getppid())
   return x*x

if __name__ == '__main__':
   print('主进程id:', os.getpid())
   p_list = []
   for i in range(5):
       p = Process(target=f, args=(i,))
       p.start()

6.3.4 多个进程同时运行

import time
from multiprocessing import Process

def f(name):
print('hello', name)
time.sleep(1)

if __name__ == '__main__':
p_list = []
for i in range(5):
p = Process(target=f, args=('tom',))
p.start()
p_list.append(p)

6.3.5 多个进程同时运行,再谈join方法

import time 
from multiprocessing import Process

def f(name):
print('hello', name)
time.sleep(1)

if __name__ == '__main__':
p_list = []
for i in range(5):
p = Process(target=f, args=('tom',))
p.start()
p_list.append(p)
for p in p_list:
p.join()
print('主进程再执行')

6.3.6 通过继承Process类开启进程

import os
from multiprocessing import Process

class MyProcess(Process):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
print(os.getpid())
print('%s正在和主播聊天' % self.name)

p1 = MyProcess('tom')
p2 = MyProcess('egon')
p3 = MyProcess('tank')

p1.start()
p2.start()
p3.start()

p1.join()
p2.join()
p3.join()

print('主进程')

6.3.7 进程之间的数据隔离问题

from multiprocessing import Process

n = 100

def work():
global n
n = 50
print('子进程:', n)

if __name__ == '__main__':
p = Process(target=work)
p.start()
print('主进程:', n)

七. 守护进程

会随着主进程的结束而结束

主进程创建守护进程

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

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

注意:进程之间是相互独立的,主进程代码运行结束,守护进程随即终止。

7.1 守护进程的启动

import os
import time
from multiprocessing import Process

class Myprocess(Process):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
print(os.getpid(),slef.name)
print('%s正在和主播聊天' % self.name)

p = Process('tom')
p.daemon = True
p.start()
time.sleep(10)
print('主')

7.2 主进程代码执行结束守护进程立即结束

from multiprocessing import Process

def foo():
print(123)
time.sleep(1)
print('end123')

def bar():
print(456)
time.sleep(3)
print('end456')

if __name__ = '__main__':
p1 = Process(target=foo)
p2 = Process(target=bar)

p1.daemon = True
p1.start()
p2.start()
time.sleep(1)
print('main......')

八. 进程同步(锁)

进程之间数据不共享,但是共享同一套同一套文件系统,所以访问同一文件,或同一个打印终端是没有问题的。而共享带来的是竞争,竞争带来的结果就是错乱,所以就要加锁处理

part1:多个进程共享同一打印终端

#并发运行,效率高,但竞争同一打印终端带来了打印错乱
from multiprocessing import Process
import os,time
def work():
print('%s is running' % os.getpid())
time.sleep(2)
print('%s is done' % os.getpid())

if __name__ == '__main__':
for i in range(3):
p = Process(target=work)
p.start()
#加锁:由并发变成串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process
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()

 

part2:多个进程共享同一文件

文件当数据库,模拟抢票

并发运行,效率高,但竞争写同一文件,数据写入错乱

from multiprocessing import Process,Lock
import time,json,random
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.2) #模拟写数据的网络延迟
json.dump(dic,open('db.txt','w'))
print('购票成功')

def task(lock):
search()
get()

if __name__ == '__main__':
lock = Lock()
for i in range(100):
p = Process(target=task,args=(lock,))
p.start()
#加锁:购票行为由并发变成了串行,牺牲了运行效率,但保证了数据安全
from multiprocess import Process
import time

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.2)
json.dump(dic,open('db.txt','w'))
print('购票成功')

def task(lock):
search()
lock.acquire()
get()
lock.release()

if __name__ == '__main__':
lock = Lock()
for i in range(100):
p = Process(target=task,args=(lock,))
p.start()

总结

#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理


#因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
1 队列和管道都是将数据存放于内存中
2 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。

九. 队列

9.1 multiprocess.Queue概念

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

Queue([maxsize])创建共享的进程队列。 参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。

底层队列使用管道和锁定实现。

9.2 方法介绍

Queue([maxsize]) 创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。 Queue的实例q具有以下方法:

q.get( [ block [ ,timeout ] ] ) 返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。

q.get_nowait()q.get(False)方法。

q.put(item [, block [,timeout ] ] ) 将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。

q.qsize() 返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。

q.empty() 如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。

q.full() 如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。

q.close() 关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。

q.cancel_join_thread() 不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。

q.join_thread() 连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。

9.3 代码实现

9.3.1 单看队列用法

from multiprocessing import Queue
q = Queue(3)

try:
q.put_nowait(3) #可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
print('队列已满')

try:
q.get_nowait(3) #可以使用get_nowait,如果队列满了不会阻塞,但是会因为没取到值而报错。
except:
print('队列已空')

9.3.2 子进程发送数据给父进程

import time
from multiprocessing import Queue

def f(q):
q.put([time.asctime(),'from Eva','hello'])

if __name__ == '__main__':
q = Queue()
p = Process(target=f,args=(q,))
p.start()
print(q.get())
p.join()

9.3.3 批量生产数据放入队列再批量获取结果

import os
import time
import multiprocessing

def inputQ(queue):
info = str(os.getpid()) + '(put):' + str(time.asctime())
queue.put(info)

def outputQ(queue):
info = q.get()
print('%s%s%s' %(srtr(os.getpid()),'get:',info))

if __name__ == '__main__':
multiprocessing.freeze_support()
record1 = []
record2 = []
queue = multiprocessing.Queue()

for i in range(10):
process = multiprocess.Process(target=inputQ,args=(queue,))
process.start()
record1.append(process)

for i in range(10):
process = multiprocess.Process(target=outputQ,args=(queue,))
process.start()
record2.append(process)

for p in record1:
p.join()

for p in record2:
p.join()

十. 生产者消费者模型

在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产者线程和消费者线程的工作能力来提高程序的整体处理数据的速度。

10.1 为什么使用生产者和消费者模式

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

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

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

10.3 基于队列实现生产者消费者模型

from multiprocessing import Process,Queue
import time,os
def consumer(q):
while True:
res = q.get()
time.sleep(random.randint(1,3))
print('%s吃%s' %(os.getpid(),res))

def producer(q):
for i in range(10):
time.sleep(random.randint(1,3))
res = '包子%s' %i
q.put(res)
print('%s生产了%s' %(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之后,则一直处于死循环中且卡在q.get()这一步。

解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环。

10.4 改良版----生产者消费者模型

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('%s吃%s' %(os.getpid(),res))
def producer(q):
for i in range(2):
time.sleep(random.randint(1,3))
res = '包子%s' %i
q.put(res)
print('%s生产了%s' %(os.getpid(),res))
q.put(None) #发送结束信号
if __name__ == '__main__':
q = Queue()
p1 = Process(target=producer,args=(q,))
c1 = Process(target=consumer,args=(q,))
p1.start()
c1.start()
print('主')

**注意:结束信号None,不一定要由生产者发出,主进程同样可以发,但主进程需要等生产者结束后才应该发送该信号。

10.5 主进程在生产者生产完毕后发送结束信号None

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('%s 吃 %s' %(os.getpid(),res))

def producer(q):
for i in range(2):
time.sleep(random.randint(1,3))
res='包子%s' %i
q.put(res)
print('生产了 %s' %(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('主')

10.6 多个消费者的例子:有几个消费者就需要发送几次结束信号

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('%s 吃 %s' %(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('生产了 %s' %(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('主')

十一. JoinableQueue([maxsize])

创建可连接的共享进程队列。这就像是一个Queue对象,但队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

11.1 方法介绍

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

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

q.join() 生产者将使用此方法进行阻塞,直到队列中所有项目均被处理。阻塞将持续到为队列中的每个项目均调用q.task_done()方法为止。 下面的例子说明如何建立永远运行的进程,使用和处理队列上的项目。生产者将项目放入队列,并等待它们被处理。

11.2 JoinableQueue队列实现消费之生产者模型

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

def producer(name,q):
for i in range(10):
time.sleep(random.randint(1,3))
print('%s吃%s' %(os.getpid(),res))
q.task_done() #生产完毕,使用此方法进行阻塞,直到队列中所有项目均被处理
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_list = [p1,p2,p3.c1,c2]
for p in p_list:
p.start()

p1.join()
p2.join()
p3.join()
print('主')

十二. 进程间数据共享

12.1 Manager模块

进程间数据独立,但可以通过Manager实现数据共享

from multiprocessing import Process,Manager,Lock
def work(d,lock):
with lock:
d['count'] -= 1
if __name__ == '__main__':
lock = Lock()
with Manager() as m:
dic = m.dic({'count':100})
p_l = []
for i in range(100):
p = Process(target=work,args=(dic,lock))
p_l.append(p)
p.start()
for p in p_l:
p.join()
print(dic)

十三. 信号量(了解)

信号量Semahpore(同线程一样)

#互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据。信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念
from multiprocessing import Process,Semaphore
import time,random

def go_wc(sem,user):
sem.acquire()
print('%s 占到一个位置' %user)
time.sleep(random.randint(0,3)) #模拟网络延迟
sem.release()

if __name__ == '__main__':
sem=Semaphore(5)
p_l=[]
for i in range(13):
p=Process(target=go_wc,args=(sem,'user%s' %i,))
p.start()
p_l.append(p)

for i in p_l:
i.join()
print('============》')

十四. 事件(了解)

Event(同线程一样)

from multiprocessing import Process,Event
import time,random

def car(e,n):
while True:
if not e.is_set(): #Flase
print('\033[31m红灯亮\033[0m,car%s等着' %n)
e.wait()
print('\033[32m车%s 看见绿灯亮了\033[0m' %n)
time.sleep(random.randint(3,6))
if not e.is_set():
continue
print('走你,car', n)
break

def police_car(e,n):
while True:
if not e.is_set():
print('\033[31m红灯亮\033[0m,car%s等着' % n)
e.wait(1)
print('灯的是%s,警车走了,car %s' %(e.is_set(),n))
break

def traffic_lights(e,inverval):
while True:
time.sleep(inverval)
if e.is_set():
e.clear() #e.is_set() ---->False
else:
e.set()

if __name__ == '__main__':
e=Event()
# for i in range(10):
# p=Process(target=car,args=(e,i,))
# p.start()

for i in range(5):
p = Process(target=police_car, args=(e, i,))
p.start()
t=Process(target=traffic_lights,args=(e,10))
t.start()

print('============》')

十五. 进程池

Pool([numprocess  [,initializer [, initargs]]]):
#创建进程池

参数介绍

1 numprocess:  要创建的进程数,如果省略,将默认使用cpu_count()的值

2 initializer: 是每个工作进程启动时要执行的可调用对象,默认为None

3 initargs: 是要传给initializer的参数组

方法介绍

1 p.apply(func [, args [, kwargs]]):  在一个池工作进程中执行func(*args,**kwargs),然后返回结果。需要强调的是:此操作并不会在所有池工作进程中并执行func函数。如果要通过不同参数并发地执行func函数,必须从不同线程调用p.apply()函数或者使用p.apply_async()

2 p.apply_async(func [, args [, kwargs]]): 在一个池工作进程中执行func(*args,**kwargs),然后返回结果。此方法的结果是AsyncResult类的实例,callback是可调用对象,接收输入参数。当func的结果变为可用时,将理解传递给callback。callback禁止执行任何阻塞操作,否则将接收其他异步操作中的结果。

3 p.close(): 关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成

4 P.join(): 等待所有工作进程退出。此方法只能在close()或teminate()之后调用
apply_async()和map_async()的返回值是AsyncResul的实例obj。实例具有以下方法
obj.get():返回结果,如果有必要则等待结果到达。timeout是可选的。如果在指定时间内还没有到达,将引发一场。如果远程操作中引发了异常,它将在调用此方法时再次被引发。
obj.ready():如果调用完成,返回True
obj.successful():如果调用完成且没有引发异常,返回True,如果在结果就绪之前调用此方法,引发异常
obj.wait([timeout]):等待结果变为可用。
obj.terminate():立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。如果p被垃圾回收,将自动调用此函数

同步调用apply

from multiprocessing import Pool
import os,time
def work(n):
print('%s run' %os.getpid())
time.sleep(3)
return n**2

if __name__ == '__main__':
p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
res_l=[]
for i in range(10):
res=p.apply(work,args=(i,)) #同步调用,直到本次任务执行完毕拿到res,等待任务work执行的过程中可能有阻塞也可能没有阻塞,但不管该任务是否存在阻塞,同步调用都会在原地等着,只是等的过程中若是任务发生了阻塞就会被夺走cpu的执行权限
res_l.append(res)
print(res_l)

异步调用apply_async

from multiprocessing import Pool
import os,time
def work(n):
print('%s run' %os.getpid())
time.sleep(3)
return n**2

if __name__ == '__main__':
p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
res_l=[]
for i in range(10):
res=p.apply_async(work,args=(i,)) #同步运行,阻塞、直到本次任务执行完毕拿到res
res_l.append(res)

#异步apply_async用法:如果使用异步提交的任务,主进程需要使用join,等待进程池内任务都处理完,然后可以用get收集结果,否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
p.close()
p.join()
for res in res_l:
print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get

详解:apply_async与apply

#一:使用进程池(异步调用,apply_async)
#coding: utf-8
from multiprocessing import Process,Pool
import time

def func(msg):
print( "msg:", msg)
time.sleep(1)
return msg

if __name__ == "__main__":
pool = Pool(processes = 3)
res_l=[]
for i in range(10):
msg = "hello %d" %(i)
res=pool.apply_async(func, (msg, )) #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
res_l.append(res)
print("==============================>") #没有后面的join,或get,则程序整体结束,进程池中的任务还没来得及全部执行完也都跟着主进程一起结束了

pool.close() #关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
pool.join() #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束

print(res_l) #看到的是<multiprocessing.pool.ApplyResult object at 0x10357c4e0>对象组成的列表,而非最终的结果,但这一步是在join后执行的,证明结果已经计算完毕,剩下的事情就是调用每个对象下的get方法去获取结果
for i in res_l:
print(i.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get

#二:使用进程池(同步调用,apply)
#coding: utf-8
from multiprocessing import Process,Pool
import time

def func(msg):
print( "msg:", msg)
time.sleep(0.1)
return msg

if __name__ == "__main__":
pool = Pool(processes = 3)
res_l=[]
for i in range(10):
msg = "hello %d" %(i)
res=pool.apply(func, (msg, )) #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
res_l.append(res) #同步执行,即执行完一个拿到结果,再去执行另外一个
print("==============================>")
pool.close()
pool.join() #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束

print(res_l) #看到的就是最终的结果组成的列表
for i in res_l: #apply是同步的,所以直接得到结果,没有get()方法
print(i)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2021-09-26 10:59  vonmo  阅读(135)  评论(0编辑  收藏  举报