众所周知,Python中不存在真正的多线程,Python中的多线程是一个并发过程。如果想要并行的执行程序,充分的利用cpu资源(cpu核心),还是需要使用多进程解决的。其中multiprocessing模块应该是Python中最常用的多进程模块了。
创建进程
基本上multiprocessing这个模块和threading这个模块用法是相同的,也是可以通过函数和类创建进程。
""" 案例1:函数式创建进程 """
import multiprocessing
import time
# 进程执行函数
def run(num):
time.sleep(1)
print(f'i am process{num}')
if __name__ == '__main__':
# 获取开始的时间戳
start = time.time()
# 存放进程的列表,用于阻塞进程
process_list = list()
# 创建4个进程,并将每个创建好的进程对象放到process_list中
for i in range(1, 5):
process = multiprocessing.Process(target=run, args=(i,))
# 启动该进程
process.start()
process_list.append(process)
# 只有进程全部结束,再向下执行
for j in process_list:
j.join()
# 结束的时间戳
end = time.time()
# 打印该程序运行了几秒
print(end-start)
# i am process1
# i am process2
# i am process3
# i am process4
# 1.122110366821289
上述案例基本上就是笔者搬用了上篇文章多线程的案例,可见其使用的相似之处。导入multiprocessing后实例化Process就可以创建一个进程,参数的话也是和多线程一样,target放置进程执行函数,args存放该函数的参数。
""" 案例2:类继承创建进程 """
import multiprocessing
import time
# 继承Process,转变为进程类
class MyProcess(multiprocessing.Process):
def __init__(self, process_id):
# 必须实现Process类的init方法
super().__init__()
self.process_id = process_id
# 重写执行函数
def run(self):
time.sleep(1)
print(f'i am process{self.process_id}')
if __name__ == '__main__':
# 获取开始的时间戳
start = time.time()
# 存放进程的列表,用于阻塞进程
process_list = list()
# 创建4个进程,并将每个创建好的进程对象放到process_list中
for i in range(1, 5):
process = MyProcess(i)
# 启动该进程
process.start()
process_list.append(process)
# 只有进程全部结束,再向下执行
for j in process_list:
j.join()
# 结束的时间戳
end = time.time()
# 打印该程序运行了几秒
print(end - start)
# i am process1
# i am process2
# i am process3
# i am process4
# 1.1184189319610596
使用类来创建进程也是需要先继承multiprocessing.Process并且实现其init方法。
进程池
Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求。
但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程。
""" 案例3:进程池Pool """
import multiprocessing
import time
# 进程执行函数
def func(msg):
print('process start...', msg)
time.sleep(3)
print('process end...')
if __name__ == '__main__':
print('ready~~~~~~~~~~~~~~~~~go~~~~~')
# 创建进程池,最大进程数量为3
pool = multiprocessing.Pool(processes=3)
for i in range(5):
msg = f'hello {i}'
# 以非阻塞的方式,维持执行的进程总数为processes并在进程结束后自动添加新的进程
pool.apply_async(func, (msg,))
# 阻止后续任务提交到进程池
pool.close()
# 等待工作进程结束
pool.join()
print('game~~~~~~~~~~~~~~~~~over~~~~~')
# ready~~~~~~~~~~~~~~~~~go~~~~~
# process start... hello 0
# process start... hello 1
# process start... hello 2
# process end...
# process start... hello 3
# process end...
# process end...
# process end...
# game~~~~~~~~~~~~~~~~~over~~~~~
需要注意的是,在调用join方法阻塞进程前,需要先调用close方法,,否则程序会出错。
在上述案例中,提到了非阻塞,当把创建进程的方法换为pool.apply(func, (msg,))时,就会阻塞进程,出现下面的状况。
# ready~~~~~~~~~~~~~~~~~go~~~~~
# process start... hello 0
# process end...
# process start... hello 1
# process end...
# process start... hello 2
# process end...
# process start... hello 3
# process end...
# process start... hello 4
# process end...
# game~~~~~~~~~~~~~~~~~over~~~~~
进程队列
在multiprocessing模块中还存在Queue对象,这是一个进程的安全队列,近似queue.Queue。队列一般也是需要配合多线程或者多进程使用。
下列案例是一个使用进程队列实现的生产者消费者模式。
""" 案例4:基于进程队列的生产者消费者模式 """
import random
import time
import multiprocessing
# 生产者
def producer(queue):
for i in range(10):
time.sleep(random.randint(1, 3))
res = f'物品{i}'
# 入队
queue.put(res)
print(f'{multiprocessing.current_process().name}生产{res}')
# 消费者
def consumer(queue):
while True:
# 出队
res = queue.get()
time.sleep(random.randint(1, 3))
print(f'{multiprocessing.current_process().name}消费{res}')
if __name__ == '__main__':
# 生产消费队列
queue = multiprocessing.Queue()
# 创建生产进程
process1 = multiprocessing.Process(target=producer, args=(queue,))
# 进程名
process1.name = 'process_1'
# 创建消费进程
process2 = multiprocessing.Process(target=consumer, args=(queue,))
# 进程名
process2.name = 'process_2'
print('程序开始!!!')
# 启动进程
process1.start()
process2.start()
# 程序开始!!!
# process_1生产物品0
# process_1生产物品1
# process_2消费物品0
# process_1生产物品2
# process_2消费物品1
# process_1生产物品3
# ......
管道
multiprocessing支持两种进程间的通信,其中一种便是上述案例的队列,另一种则称作管道。在官方文档的描述中,multiprocessing中的队列是基于管道实现的,并且拥有更高的读写效率。
管道可以理解为进程间的通道,使用Pipe([duplex])创建,并返回一个元组(conn1,conn2)。如果duplex被置为True(默认值),那么该管道是双向的,如果duplex被置为False,那么该管道是单向的,即conn1只能用于接收消息,而conn2仅能用于发送消息。
其中conn1、conn2表示管道两端的连接对象,每个连接对象都有send()和recv()方法。send和recv方法分别是发送和接受消息的方法。例如,可以调用conn1.send发送消息,conn1.recv接收消息。如果没有消息可接收,recv方法会一直阻塞。如果管道已经被关闭,那么recv方法会抛出EOFError。
""" 案例5:管道 """
import multiprocessing
import random
import time
# 发送数据
def proc_send(pipe, data):
for d in data:
pipe.send(d)
print(f'{multiprocessing.current_process().name}发送了数据{d}')
time.sleep(random.random())
# 接收数据
def proc_recv(pipe):
while True:
print(f'{multiprocessing.current_process().name}接收了数据{pipe.recv()}')
time.sleep(random.random())
if __name__ == '__main__':
# 创建管道
pipe1, pipe2 = multiprocessing.Pipe()
# 数据
data = [i for i in range(10)]
# 创建进程process1
process1 = multiprocessing.Process(target=proc_send, args=(pipe1, data))
process1.name = 'process_1'
# 创建进程process1
process2 = multiprocessing.Process(target=proc_recv, args=(pipe2,))
process2.name = 'process_2'
# 启动进程
process1.start()
process2.start()
# 阻塞进程
process1.join()
process2.join()
# process_1发送了数据0
# process_2接收了数据0
# process_1发送了数据1
# process_2接收了数据1
# process_1发送了数据2
# process_2接收了数据2
# process_1发送了数据3
# process_2接收了数据3
# ......
关于multiprocessing模块其实还有很多实用的类和方法,由于篇幅有限(懒),笔者就先写到这里。该模块其实用起来很像threading模块,像锁对象和守护线程(进程)等multiprocessing模块也是有的,使用方法也近乎相同。
如果想要更加详细的了解multiprocessing模块,请参考官方文档。
# multiprocessing --- 基于进程的并行
https://docs.python.org/zh-cn/3/library/multiprocessing.html