因为工作原因很长时间没有维护博客了,先把最近工作中的技术内容记录下。最近在做摄像头和激光雷达融合的目标检测,数据输入模型之前要做不少预处理的工作,严重影响检测的实时性,所以考虑将预处理部分采用多任务并行处理。
1. 多进程介绍
进程是资源的集合,是最小的资源单位。是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
讲解多进程之前我们先来熟悉一下并行和并发的概念:
并行处理:指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。并行处理的主要目的是节省大型和复杂问题的解决时间。
并发处理:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。
无论是并行还是并发,在用户看来都是'同时'运行的,不管是进程还是线程,真是干活的是cpu,而一个cpu同一时刻只能执行一个任务。对于“并发”而言,是伪并行,即看起来是同时运行,单个cpu+多道技术就可以实现并发;而“并行”才是真正意义上的“同时运行”——仅有多核才能够实现“并行”。采用多进程可以实现并行的效果,充分的使用多核CPU的资源,对IO密集型的操作可以很好利用IO阻塞的时间。
2. 多进程的创建
python中的多进程是使用multiprocessing模块实现,mutiprocessing模块支持创建子进程,并在子进程中执行各自的任务;支持数据共享和通信;支持不同形式的同步;提供Process、Manager、Pool、Queue、Lock等组件。
2.1Process模块
Process模块的方法有:
start():启动进程,并调用该子进程中的p.run()
run():进程启动时运行的方法,它去调用target指定的函数,我们自定义类的类中一定要实现该方法。
terminate():强制终止进程。不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程。
is_alive():判断进程是否是“活着”的状态。
join:在当前位置阻塞主进程,等待执行join的子进程结束后再继续执行主进程。
创建并开启子进程的两种方法
# 方法一 普通形式 import os from multiprocessing import Process def fun_1(str): print("子进程id:", os.getpid(), str) def main(): p1 = Process(target=fun_1, args=('this is process 1',)) p2 = Process(target=fun_1, args=('this is process 2',)) p3 = Process(target=fun_1, args=('this is process 3',)) p1.start() p2.start() p3.start() print('主进程id:', {os.getpid()}) if __name__ == '__main__': main()
运行结果:
主进程id: {5247}
子进程id: 5254 this is process 1
子进程id: 5255 this is process 2
子进程id: 5256 this is process 3
# 方法二 继承形式 import os from multiprocessing import Process class subProcess(Process): def __init__(self, str): super().__init__() self.str = str def run(self): print("子进程id:",os.getpid(),self.str) def main(): p1 = subProcess('this is process 1') p2 = subProcess('this is process 2') p3 = subProcess('this is process 3') # start函数会调用run执行 p1.start() p2.start() p3.start() print("主进程id:", os.getpid()) if __name__ == '__main__': main()
运行结果:
主进程id: 5616
子进程id: 5621 this is process 2
子进程id: 5620 this is process 1
子进程id: 5622 this is process 3
每个进程都有一个虚拟的地址空间、可执行代码、上下文空间、进程ID、环境变量等。
from multiprocessing import Process import time x = 100 def fun(): global x x = 0 print('子进程x:%s, 内存地址%s'%(x, id(x))) if __name__ == '__main__': p = Process(target=fun) p.start() time.sleep(2) global x print('主进程x:%s, 内存地址%s'%(x, id(x)))
进程之间的内存是相互独立的,所以子进程修改x的值不会影响到主进程:
子进程x:0, 内存地址10919296
主进程x:100, 内存地址10922496
将数据处理划分成多个任务采用多进程方式处理可压缩预处理时间,下面将数据的预处理分成三个任务:图像处理子任务、点云处理子任务、anchor生成子任务,总耗时等于最慢子任务耗时+进程创建/销毁时间。实例如下:
from multiprocessing import Process import os import time def imgProcess(index): start = time.time() index = index + 1 time.sleep(1) print("子进程id:%s, 开始时间%s, 运行时间%s:"%(os.getpid(), start, time.time()-start)) return index def lidarProcess(index): start = time.time() index = index + 2 time.sleep(2) print("子进程id:%s, 开始时间%f, 运行时间%s:" % (os.getpid(), start, time.time() - start)) return index def anchorProcess(index): start = time.time() index = index + 3 time.sleep(3) print("子进程id:%s, 开始时间%s, 运行时间%s:" % (os.getpid(), start, time.time() - start)) return index if __name__ == '__main__': start = time.time() index = 0 ret = [] # image process ps_image = Process(target=imgProcess, args=(index,)) ps_image.start() # velodyne process ps_lidar = Process(target=lidarProcess, args=(index,)) ps_lidar.start() # anchor process ps_anchor = Process(target=anchorProcess, args=(index,)) ps_anchor.start() ret.append(ps_image) ret.append(ps_lidar) ret.append(ps_anchor) for ps_ret in ret: ps_ret.join() print('主进程运行时间:%s'%(time.time()-start))
运行结果:
子进程id:9888, 开始时间1567311112.7952406, 运行时间1.0003292560577393:
子进程id:9889, 开始时间1567311112.789489, 运行时间2.002042531967163:
子进程id:9890, 开始时间1567311112.8022897, 运行时间3.0023183822631836:
主进程运行时间:3.1324758529663086
相比于串行处理能够压缩掉图像的预处理时间开销和点云的预处理时间开销。
3. 多进程同步
多进程可以协同工作来完成一项任务,通常需要共享数据。所以在多进程之间保持数据的一致性就很重要,需要共享数据协同的进程必须以适当的策略来读写数据。同步原语和线程库类似。
Lock:一个Lock对象有两个方法acquire和release来控制共享数据的读写权限。
Event:一个进程发事件的信号,另一个进程等待事件的信号。Event对象有两个方法set和clear来管理自己内部的变量。
Semaphore:用来共享资源,比如:支持固定数据的共享连接。
共享变量:multiprocessing模块提供了Array/Manager/Value类,能够实现进程间共享数字变量/字符串变量/列表/字典/实例对象。
Condition:同步部分工作流程,有两个基本的方法,wait()用来等待进程,notify_all用来通知所有等待此条件的进程。
Barrier:将程序分成几个阶段,适用于有些进程必须在某些特性进程之后执行,处于Barrier之后的代码不能同处于Barrier之前的代码并行。
3.1 Lock
锁:通俗的说就是每次程序只让一个子进程拿钥匙开锁,并将门关上,其他进程没有钥匙就无法进入,当程序执行完毕将钥匙放回,下一个子进程才能拿钥匙进去。我们来写一个银行存钱的实例程序:
from multiprocessing import Process, Lock, Value import time # 银行取钱,本金100元,每次取1元,取100次 def get_money(num, lock): lock.acquire() for i in range(100): num.value -= 1 print(num.value) time.sleep(0.01) lock.release() # 银行存钱,本金100元,每次存1元,存100次 def put_money(num, lock): lock.acquire() for i in range(100): num.value += 1 print(num.value) time.sleep(0.01) lock.release() if __name__ == '__main__': # 共享数据num表示100软民比 num = Value('i', 100) l = Lock() ps_get = Process(target=get_money, args=(num, l)) ps_get.start() ps_put = Process(target=put_money, args=(num, l)) ps_put.start() ps_get.join() ps_put.join() print(num.value)
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1、效率低
2、需要自己加锁处理
为此multiprocessing模块为我们提供了基于消息的IPC通信机制:队列和管道。
1、队列和管道都是将数据存放与内存中。
2、队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来。
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
3.2 Semaphore
刚才说的Lock属于互斥锁,就是一把钥匙配备一把锁,同时只允许锁住某一个数据。而信号量则是多把钥匙配备多把锁,也就是说同时允许锁住多个数据。信号量同步基于内部计数器,用户初始化一个计数器初值,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1。当计数器为0时,acquire()调用被阻塞。这是信号量概念中P和V操作的Python实现。信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念。
通俗的说在一个餐厅里,有10张餐桌,那么这个店最多就同时允许进入10桌的客人同时就餐,当有第11对的客人来的时候,就需要在门外取号等待;当某一桌客人吃完离开后,才允许再进来一桌顾客。
import multiprocessing import time def dining(s, i): s.acquire() print(multiprocessing.current_process().name + " acquire") time.sleep(i) print(multiprocessing.current_process().name + " release") s.release() if __name__ == "__main__": # 设置限制最多10个进程同时访问共享资源 s = multiprocessing.Semaphore(10) for i in range(20): p = multiprocessing.Process(target = dining, args = (s, i * 2)) p.start()
最多只能有10个进程同时获得锁
3.3 Event
Event提供一种简单的方法,可以在进程间传递状态信息,实现进程间同步通信。事件可以切换设置和未设置状态。通过使用一个可选的超时值,事件对象的用户可以等待其状态从未设置变为设置。
import multiprocessing import time def wait_for_event(e): """Wait for the event to be set before doing anything""" print('wait_for_event: starting') e.wait() # 等待收到能执行信号,如果一直未收到将一直阻塞 print('wait_for_event: e.is_set()->', e.is_set()) def wait_for_event_timeout(e, t): """Wait t seconds and then timeout""" print('wait_for_event_timeout: starting') e.wait(t) # 等待t秒超时,此时Event的状态仍未未设置,继续执行 print('wait_for_event_timeout: e.is_set()->', e.is_set()) e.set() # 初始内部标志为真 if __name__ == '__main__': e = multiprocessing.Event() print("begin,e.is_set()", e.is_set()) w1 = multiprocessing.Process(name='block', target=wait_for_event, args=(e,)) w1.start() w2 = multiprocessing.Process(name='block', target=wait_for_event, args=(e,)) w2.start() # 可将2改为5,看看执行结果 w3 = multiprocessing.Process(name='nonblock', target=wait_for_event_timeout, args=(e, 2)) w3.start() print('main: waiting before calling Event.set()') time.sleep(3) # e.set() #可注释此句话看效果 print('main: event is set')
运行结果:两个等待的进程都继续执行
begin,e.is_set() False
main: waiting before calling Event.set()
wait_for_event: starting
wait_for_event_timeout: starting
wait_for_event: starting
wait_for_event_timeout: e.is_set()-> False
wait_for_event: e.is_set()-> True
wait_for_event: e.is_set()-> True
main: event is set
3.4 Manager类
进程与进程相互独立,每个进程都是各自的地址空间,所以用一般的方法是不能共享变量的,但是多进程程序对同一个资源进行操作还可以使用共享变量完成,multiprocess包中封装了进程间共享数据的三个类Value、Array、Manager,三者之间的区别主要在于管理的数据类型不同。
Python中的Manager模块返回的对象控制了一个server进程,此进程包含的python对象可以被其他的进程通过proxies来访问。从而达到多进程间数据通信且安全,manager支持的类型非常多,包括:Value、Array、list、dict、Queue、Lock等。
from multiprocessing import Process, Manager import os import time def imgProcess(index, dict, key): start = time.time() index = index + 10 time.sleep(1) print("子进程id:%s, 开始时间%s, 运行时间%s:"%(os.getpid(), start, time.time()-start)) dict[key] = index def lidarProcess(index, dict, key): start = time.time() index = index + 20 time.sleep(2) print("子进程id:%s, 开始时间%f, 运行时间%s:" % (os.getpid(), start, time.time() - start)) dict[key] = index def anchorProcess(index, dict, key): start = time.time() index = index + 30 time.sleep(3) print("子进程id:%s, 开始时间%s, 运行时间%s:" % (os.getpid(), start, time.time() - start)) dict[key] = index if __name__ == '__main__': start = time.time() index = 0 ret = [] manager = Manager() mDict = manager.dict() # image process ps_image = Process(target=imgProcess, args=(index, mDict, "image", )) ps_image.start() # velodyne process ps_lidar = Process(target=lidarProcess, args=(index, mDict, "lidar", )) ps_lidar.start() # anchor process ps_anchor = Process(target=anchorProcess, args=(index, mDict, "anchor",)) ps_anchor.start() ret.append(ps_image) ret.append(ps_lidar) ret.append(ps_anchor) for ps_ret in ret: ps_ret.join() print('Results:') for key in dict(mDict): print("%s=%s" % (key, mDict[key])) print('主进程运行时间:%s'%(time.time()-start))
通过dict的形式返回结果如下:
子进程id:13625, 开始时间1567315148.3165178, 运行时间1.0011928081512451:
子进程id:13626, 开始时间1567315148.314636, 运行时间2.002166509628296:
子进程id:13627, 开始时间1567315148.3179827, 运行时间3.0009241104125977:
Results:
anchor=30
lidar=20
image=10
主进程运行时间:3.2750587463378906
3.5 Value和Array类
实现父进程和子进程的共享内存,还可以使用Value或Array创建共享对象:
from multiprocessing import Process, Value, Array def fun(n, a): n.value = 3.14 a[0] = 5 if __name__ == '__main__': num = Value('d', 0.0) arr = Array('i', range(10)) print("子进程修改前num:", num.value) print("子进程修改前arr:", arr[:]) ps = Process(target=fun, args=(num, arr)) ps.start() ps.join() print("子进程修改后num:", num.value) print("子进程修改后arr:", arr[:])
运行结果:
子进程修改前num: 0.0
子进程修改前arr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
子进程修改后num: 3.14
子进程修改后arr: [5, 1, 2, 3, 4, 5, 6, 7, 8, 9]
4 进程间通信
由于进程之间数据是不共享的,所以不会出现多线程GIL带来的问题。multiprocessing包支持两种形式多进程之间的通信:Queue和Pipe,这两种方式都是使用消息传递的。
4.1 Queue模块
上一小结讲解了多进程同步的问题,多进程的并行编程的另一大问题——进程间通信。Queue创建共享的进程队列,是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。通常在生产者消费者模式中经常使用,一个进程向队列中写数据,另一个进程从队列中取数据。
Queue模块的方法有:
qsize():返回当前队列包含的消息数量;
empty():如果队列为空,返回True,反之False ;
full():如果队列满了,返回True,反之False;
get():获取队列中的一条消息,然后将其从列队中移除,可传参超时时长;
get_nowait():相当Queue.get(False)
,取不到值时触发异常:Empty;
put():将一个值添加进数列,可传参超时时长;
put_nowait():相当于Queue.get(False)
,当队列满了时报错:Full;
我们新开一个Detection进程,并且将预处理完成的数据存入队列,Detection进程从队列Queue获取待检测的数据:
from multiprocessing import Process, Manager, Queue import os import time def imgProcess(index, dict, key): start = time.time() index = index + 10 time.sleep(1) print("子进程id:%s, 开始时间%s, 运行时间%s:"%(os.getpid(), start, time.time()-start)) dict[key] = index def lidarProcess(index, dict, key): start = time.time() index = index + 20 time.sleep(2) print("子进程id:%s, 开始时间%f, 运行时间%s:" % (os.getpid(), start, time.time() - start)) dict[key] = index def anchorProcess(index, dict, key): start = time.time() index = index + 30 time.sleep(3) print("子进程id:%s, 开始时间%s, 运行时间%s:" % (os.getpid(), start, time.time() - start)) dict[key] = index def detect(queue): while True: if not queue.empty(): mDict = queue.get() print('Results:') for key in dict(mDict): print("%s=%s" % (key, mDict[key])) else: continue def dataProcess(mQueue): while True: start = time.time() index = 0 ret = [] manager = Manager() mDict = manager.dict() # image process ps_image = Process(target=imgProcess, args=(index, mDict, "image",)) ps_image.start() # velodyne process ps_lidar = Process(target=lidarProcess, args=(index, mDict, "lidar",)) ps_lidar.start() # anchor process ps_anchor = Process(target=anchorProcess, args=(index, mDict, "anchor",)) ps_anchor.start() ret.append(ps_image) ret.append(ps_lidar) ret.append(ps_anchor) for ps_ret in ret: ps_ret.join() mQueue.put(mDict) print('主进程运行时间:%s' % (time.time() - start)) if __name__ == '__main__': # 创建一个队列存储共享数据 mQueue = Queue() # 创建一个消费者进程从队列mQueue中读取数据 ps_detect = Process(target=detect, args=(mQueue,)) ps_detect.start() # 主进程做为生产者,将三个子进程的预处理结果存入队列mQueue dataProcess(mQueue)
主进程不断的生产数据存入队列,而detect进程不断轮寻mQueue队列,当里面有数据,就取出来执行检测任务。有的读者可能疑惑对Queue进行读写操作是否需要加锁,那么原则上如果有多个进程在同一时刻获取Queue中的数据这件事情,是需要加锁防止数据混乱,但是Queue本身时进程安全的,在底层已经通过管道加锁的方式进行了内部保护。
4.2 Pipe模块
Pipe的本质是进程之间的数据传递,而不是数据共享,这和socket有点像。pipe()返回两个连接对象分别表示管道的两端,每端都有send()和recv()方法。如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据。
# 创建实例
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
# 参数介绍
dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
# 方法介绍
conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法
conn1.fileno():返回连接使用的整数文件描述符
conn1.poll([timeout]):如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。
conn1.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,
并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。
conn.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收
conn1.recv_bytes_into(buffer [, offset]):接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。
offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。
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异常。因此在生产者中关闭管道不会有任何效果,付费消费者中也关闭了相同的管道端点。
5 进程池
创建多进程的目的是为了并发,通常有几个核就会开几个进程,不过由于进程启动的开销比较大,使用多进程的时候会导致大量内存空间被消耗并且需要并发执行的任务要远大于核数,这时我们就可以通过维护一个进程池来控制进程数目。
当被操作对象数目不大时,可以直接利用Process动态成生多个进程,但如果是上百个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。Pool模块可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。
进程池中常用方法:apply()
:同步执行(串行)apply_async()
: 异步执行(并行)terminate()
:立刻关闭进程池join()
:主进程等待所有子进程执行完毕。必须在close或terminate()之后。close()
:等待所有进程结束后,才关闭进程池。
5.1 同步调用
apply同步执行:阻塞式
#_*_coding:utf-8_*_ import time from multiprocessing import Pool import os import cv2 def fun_1(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(1) return image def fun_2(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(2) return image def fun_3(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(3) return image def fun_4(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(4) return image def fun_5(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(5) return image if __name__ == "__main__": image_path = '/home/chenwei/chenwei/test/data/images/000000.png' image = cv2.imread(image_path) print("父进程 (%s) 启动 (%s) 个cpu核" % (os.getpid(), os.cpu_count())) ps = Pool(os.cpu_count()) # 定义一个进程池,最大的进程池为cpu核数 index = 0 while True: start = time.time() # 异步apply_async用法:如果使用异步提交的任务,主进程需要使用join,等待进程池内任务都处理完,然后可以用get收集结果 p1 = ps.apply(func=fun_1, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p2 = ps.apply(func=fun_2, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p3 = ps.apply(func=fun_3, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p4 = ps.apply(func=fun_4, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p5 = ps.apply(func=fun_5, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 index = index + 1 # 使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get # image = p1.get() # image = p2.get() # image = p3.get() # image = p4.get() # image = p5.get() print("main time is", time.time() - start) ps.close() # 关闭进程池,关闭后po不再接受新的请求 ps.join() # 等待ps中的所有子进程执行完成,必须放在close语句之后 '''如果没有添加join(),会导致有的代码没有运行就已经结束了''' print("-----end-----")
运行结果:
父进程 (21371) 启动 (12) 个cpu核
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21391)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21392)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21396)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21400)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21402)
main time is 15.123281478881836
同步执行耗时等于所有子进程耗时之和。
5.2 异步调用
apply_async异步执行:非阻塞
#_*_coding:utf-8_*_ import time from multiprocessing import Pool import os import cv2 def fun_1(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(1) return image def fun_2(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(2) return image def fun_3(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(3) return image def fun_4(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(4) return image def fun_5(image, index): print("此刻运行的函数是由子进程运行, index是{%s} 子进程ID是(%s)" % (index, os.getpid())) time.sleep(5) return image if __name__ == "__main__": image_path = '/home/chenwei/chenwei/test/data/images/000000.png' image = cv2.imread(image_path) print("父进程 (%s) 启动 (%s) 个cpu核" % (os.getpid(), os.cpu_count())) ps = Pool(os.cpu_count()) # 定义一个进程池,最大的进程池为cpu核数 index = 0 while True: start = time.time() # 异步apply_async用法:如果使用异步提交的任务,主进程需要使用join,等待进程池内任务都处理完,然后可以用get收集结果 p1 = ps.apply_async(func=fun_1, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p2 = ps.apply_async(func=fun_2, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p3 = ps.apply_async(func=fun_3, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p4 = ps.apply_async(func=fun_4, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 p5 = ps.apply_async(func=fun_5, args=(image, str(index))) #创建子进程实例,并关联函数和传入参数 index = index + 1 # 使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get image = p1.get() image = p2.get() image = p3.get() image = p4.get() image = p5.get() print("main time is", time.time() - start) ps.close() # 关闭进程池,关闭后po不再接受新的请求 ps.join() # 等待ps中的所有子进程执行完成,必须放在close语句之后 '''如果没有添加join(),会导致有的代码没有运行就已经结束了''' print("-----end-----")
运行结果:
父进程 (21530) 启动 (12) 个cpu核
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21549)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21550)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21554)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21559)
此刻运行的函数是由子进程运行, index是{0} 子进程ID是(21564)
main time is 5.097668409347534
异步执行耗时等于处理最慢的子进程耗时。
5.3 apply_sync和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) #看到的是
对象组成的列表,而非最终的结果,但这一步是在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)
最后总结一下吧,在Python里面由于GIL的关系,多线程很鸡肋,如果想多并行处理,大多数情况只能采用多进程的方案。至于多线程编程,下次打算写篇C++版本的多线程。