在Python中执行数据处理任务时,可能执行非常缓慢,这时可以将一个进程任务拆分为多个子进程,利用CPU的多个核心并发执行多个进程的方式来加速程序的执行。python中用于处理多进程相关的包为multiprocessing,通过Process、Queue、Pipe、Lock等类实现子进程、通信和共享数据、进程同步等功能。
1、进程的创建和执行
有两种创建子进程的方式,第一种是直接通过Process()
创建子进程对象,第二种是通过继承multiprocessing.Process
类的方式,先创建子进程类然后再实例化子进程对象。
Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None),其中target为子进程要执行的函数名称,args以元组的方式为函数传入参数。生成的对象通过start()
开始执行进程,is_alive()
返回是否存活,通过pid
、name
属性获取进程id和名字。join()
会阻塞调用进程指导本进程执行结束,terminate()
终止进程。
如下所示,创建多个子进程并启动,注意不可以通过for循环来启动子进程,那样子进程会依次执行,而不是并发。程序运行后等待两秒后一同输出“第1/2/3个子进程”
import time
from multiprocessing import Process
def process_func(num):
time.sleep(2)
print('第', num, '个进程')
if __name__ == '__main__':
process_list = []
# 循环创建多个子进程
for i in range(3):
p = Process(target=process_func, args=(i+1,))
process_list.append(p)
# 依次启动进程
process_list[0].start()
process_list[1].start()
process_list[2].start()
如下通过类继承的方式创建子进程类,在__init__()
方法中接收参数,并重写run()
方法来定义子进程所需要执行的操作
class ClockProcess(Process):
def __init__(self, num): # 接收参数
Process.__init__(self)
self.num = num
def run(self): # 定义子进程执行的操作
time.sleep(2)
print('第', self.num, '个进程')
if __name__ == '__main__':
p4 = ClockProcess(4)
p4.start()
守护进程:如果子进程的daemon
属性设置为True,则其在主进程结束后会自动结束子进程,
def process_func(num):
time.sleep(2)
print('第', num, '个进程')
if __name__ == '__main__':
p = Process(target=process_func, args=(5,))
# p.daemon = True # 在start()之前开启daemon属性
p.start()
print('主进程结束')
上面的程序运行结果如下,主进程在开启子进程后继续运行,输出“主进程结束”,子进程继续执行,先休眠两秒,然后输出“第 5 个进程”
主进程结束
第 5 个进程
但是在设置p.daemon = True
,运行只输出“主进程结束”,子进程不会输出,说明主进程结束后子进程也随之结束
进程阻塞:通过join()
方法可以阻塞调用进程,直到子进程运行结束,例如下面程序在使用p.join()阻塞主进程,主进程会等待子进程运行结束输出“第 5 个进程”,再继续执行输出“主进程结束”,
if __name__ == '__main__':
p = Process(target=process_func, args=(5,))
p.daemon = True
p.start()
p.join() # 阻塞进程
print('主进程结束')
'''
第 5 个进程
主进程结束
'''
2、互斥与同步
Lock锁
multiprocessing提供了Lock类用于实现进程锁机制,可以使用with
的方式来进行锁管理,或者手动使用lock.acquire()
、lock.release()
来获取或者释放锁。如下所示连个进程分别对文件进行写入,通过锁管理,实现了一个进程写完之后,另一个进程再写入
import multiprocessing
def prcess1(lock, f):
with lock: # 使用with进行锁管理
fs = open(f, 'a+')
n = 3
while n > 1:
fs.write("进程1写入文件\n")
n -= 1
fs.close()
def process2(lock, f):
lock.acquire() # 获取锁
try:
fs = open(f, 'a+')
n = 3
while n > 1:
fs.write("进程2写入文件\n")
n -= 1
fs.close()
finally:
lock.release() # 释放锁
if __name__ == "__main__":
lock = multiprocessing.Lock()
f = "file.txt"
w = multiprocessing.Process(target=prcess1, args=(lock, f))
nw = multiprocessing.Process(target=process2, args=(lock, f))
w.start()
nw.start()
'''
file.txt
进程2写入文件
进程2写入文件
进程1写入文件
进程1写入文件
'''
信号量Semaphore
为了互斥对多个同类资源的访问,引入了信号量机制,它和锁不同之处在于Lock每次只允许一个进程获得锁,Semaphore
可以指定同时有多个进程获得资源
import multiprocessing
import time
def worker(s):
s.acquire()
print(multiprocessing.current_process().name + "acquire")
time.sleep(2)
print(multiprocessing.current_process().name + "release\n")
s.release()
if __name__ == "__main__":
s = multiprocessing.Semaphore(2) # 最大资源数为2
for i in range(5):
p = multiprocessing.Process(target=worker, args=(s,))
p.start()
'''
Process-2acquire
Process-3acquire
Process-3release
Process-2release
Process-1acquire
Process-4acquire
Process-1release
Process-4release
Process-5acquire
Process-5release
'''
Event同步
multiprocessing提供了Event
用于实现进程之间的同步,例如下面的进程2开始执行后,通过wait()
等待event,进程1通过set()
触发event后,进程2再继续执行
def process1(e):
time.sleep(2)
e.set() # 通过set()触发event
print('子进程1触发event')
def process2(e):
print('进程2等待中。。。')
e.wait() # 等待event被触发
print('子进程2继续执行')
if __name__ == "__main__":
e = multiprocessing.Event()
p1 = multiprocessing.Process(target=process1, args=(e,))
p2 = multiprocessing.Process(target=process2, args=(e,))
p1.start()
p2.start()
print('主进程结束')
'''
主进程结束
进程2等待中。。。
子进程1触发event
子进程2继续执行
'''
3、进程通信
队列Queue
multiprocessing提供了Queue类来实现进程之间数据的传递。
通过put()
将数据放入队列,当队列满时,如果属性block=False
会立即抛出异常queues.Full
;若block为True(默认值),会等待timeout
的时间再尝试放入,失败后抛出异常。
通过get()
从队列取出数据,同样地在队列为空时,若block=False会抛出异常queues.Empty
;block=True会等待timeout的时间取出
如下所示写进程write_process每隔两秒写入队列,读进程read_process接收数据
def write_process(q):
try:
for i in range(3):
time.sleep(2)
q.put(1, block=True, timeout=2) # 将数据放入队列
except multiprocessing.queues.Full:
print('队列已满')
def read_process(q):
try:
for i in range(3):
msg = q.get() # 从队列读取数据
print('读进程接收到:', msg)
except multiprocessing.queues.Empty:
print('队列为空')
if __name__ == "__main__":
q = multiprocessing.Queue(2) # 大小为2的队列
p1 = multiprocessing.Process(target=write_process, args=(q,))
p2 = multiprocessing.Process(target=read_process, args=(q,))
p1.start()
p2.start()
print('主进程结束')
'''
主进程结束
读进程接收到: 1
读进程接收到: 1
读进程接收到: 1
'''
管道Pipe
与只能一端写、一端读的Queue相比,Pipe在全双工模式下两端可以同时进行读写。其构造方法Pipe()
会返回管道的两端(pipe1, pipe2)
, 默认指定属性duplex=True
开启全双工模式,这时pipe1、pipe2都可以进行读写。若设置duplex=False
,则pipe1只能读,pipe2只能写。
pipe通过send()
发送信息,recv()
接收信息。
def process1(pipe):
for i in range(3):
pipe.send(i)
time.sleep(2)
print('进程1收到信息:', pipe.recv())
def process2(pipe):
for i in range(3):
pipe.send(i)
time.sleep(2)
print('进程2收到信息:', pipe.recv())
if __name__ == "__main__":
(pipe1, pipe2) = multiprocessing.Pipe() # 全双工模式管道
p1 = multiprocessing.Process(target=process1, args=(pipe1,))
p2 = multiprocessing.Process(target=process2, args=(pipe2,))
p1.start()
p2.start()
'''
进程2收到信息: 0
进程1收到信息: 0
进程1收到信息: 1
进程2收到信息: 1
进程2收到信息: 2
进程1收到信息: 2
'''
4、进程池Pool
当进程数量较少时,我们可以手动创建分配进程的执行,但是当进程数量较多时,手动分配就不太现实,而且处理器同时可以执行的进程数是有限制的,这时可以使用进程池来自动分配进程资源。当进程池中有空闲的进程资源时,就会自动分配给请求并执行,如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会分配新的进程给请求。
通过Pool()
来创建进程池,processes
属性指定最大进程池数。通过apply_async()
以异步的方式分配进程,apply()以阻塞的方式。close()
关闭进程池
如下所示,首先创建大小为2的进程池,然后循环提交3个进程请求,由于进程池大小为2,进程0、1先执行,结束后,进程池再分配资源执行进程2
def process(i):
print('执行进程:', i)
time.sleep(2)
print('进程', i, '执行结束')
if __name__ == "__main__":
pool = multiprocessing.Pool(processes=2) # 创建大小为2的进程池
for i in range(3):
pool.apply_async(func=process, args=(i,)) # 异步的方式分配进程
print('主进程继续执行')
pool.close() # 关闭进程池
pool.join() # 阻塞主进程直到进程池运行结束
print('主进程执行结束')
'''
主进程继续执行
执行进程: 0
执行进程: 1
进程 0 执行结束
进程 1 执行结束
执行进程: 2
进程 2 执行结束
主进程执行结束
'''
所谓异步调用是指发起任务后必须不用等待执行任务,可以立即开启执行其他操作,相对地同步调用是发起任务后必须在原地等待任务执行完成,才能继续执行。上面的例子中使用apply_async()
以异步的方式分配进程,可见分配进程之后主进程不等待子进程的执行,输出“主进程继续执行”。当采用apply()
分配进程时,执行顺序如下,可见进程0开始执行后并没有继续执行其他进程,而是等待执行结束后才继续执行进程1.这样的方式并不能利用多进程并发的优势。
执行进程: 0
进程 0 执行结束
执行进程: 1
进程 1 执行结束
执行进程: 2
进程 2 执行结束
主进程继续执行
主进程执行结束