读书笔记
《Python并行编程实战》 第3章 基于进程的并行
multiprocessing
该模块是Python语言标准库的一部分,用于实现基于进程的并行。
1 理解multiprocessing模块
要求子进程能够进入main模块,即’__main__’
2 创建进程
2.1 准备工作
创建进程的步骤:
- 定义Process对象
- 调用进程的start()方法运行这个进程
- 调用进程的join()方法。它会等待,直到这个进程完成任务然后退出。
2.2 实现
import multiprocessing
def myFunc(i):
print("calling myFunc from process n : %s" % i)
for j in range(0,i):
print('output from myFunc is : %s' %j)
if __name__ == '__main__':
for i in range(6):
process = multiprocessing.Process(target = myFunc, args=(i,))
process.start()
process.join()
如果没有join方法,子进程不会结束,必须手动杀死。
2.3 输出与原理
calling myFunc from process n : 0
calling myFunc from process n : 1
output from myFunc is : 0
calling myFunc from process n : 2
output from myFunc is : 0
output from myFunc is : 1
calling myFunc from process n : 3
output from myFunc is : 0
output from myFunc is : 1
output from myFunc is : 2
calling myFunc from process n : 4
output from myFunc is : 0
output from myFunc is : 1
output from myFunc is : 2
output from myFunc is : 3
calling myFunc from process n : 5
output from myFunc is : 0
output from myFunc is : 1
output from myFunc is : 2
output from myFunc is : 3
output from myFunc is : 4
- Process创建的参数主要是函数myFunc和要传入的参数args。
- 调用process.start()开始进程
- 调用process.join()会等待直到子进程结束才继续执行主进程
- 为什么一定要有main模块?因为在执行进程时会无限次导入函数所在脚本,如果不用main模块,则会无限递归调用。解决这个问题的另一个方法是把子进程与主进程写在不同的文件中。
3 命名进程
3.1 准备工作
可以用multiprocessing.current_process()来访问正在运行的进程的一些属性。name属性来标识其名字。
3.2 实现
import multiprocessing
import time
def myFunc():
name = multiprocessing.current_process().name
print("Starting process name = %s\n" % name)
time.sleep(3)
print("Exiting process name = %s" % name)
if __name__ == '__main__':
process_with_name = multiprocessing.Process(name='myFunc process', target=myFunc)
process_without_default_name = multiprocessing.Process(target=myFunc)
process_with_name.start()
process_without_default_name.start()
# 主进程阻塞直到子进程完成
process_with_name.join()
process_without_default_name.join()
3.3 输出
Starting process name = myFunc process
Starting process name = Process-2
Exiting process name = Process-2
Exiting process name = myFunc process
- 可以在创建进程时使用name来传递自定义的名字
- 可以使用multiprocessing.current_process()来访问当前进程的属性
4 守护进程(daemon)
4.1 准备工作
守护进程即后台运行的进程,将process.daemon赋值为True即可在后台运行进程。
4.2 实现
import multiprocessing
import time
def foo():
name = multiprocessing.current_process().name
print('Starting %s \n' % name)
if name=='background_process':
for i in range(5):
print('--> %d\n' %i)
time.sleep(1)
else:
for i in range(5,10):
print('--> %d\n' %i)
time.sleep(1)
print('Exiting %s\n' %name)
if __name__ == '__main__':
background_proess = multiprocessing.Process\
(name='background_process',
target=foo)
background_proess.daemon = True
No_background_process = multiprocessing.Process\
(name='No_background_process',
target=foo)
No_background_process.daemon = False
background_proess.start()
No_background_process.start()
4.3 输出
Starting No_background_process
--> 5
--> 6
--> 7
--> 8
--> 9
Exiting No_background_process
- 可以看到后台进程没有任何输出
- 可以在创建process后,用process.daemon=True来将其设为后台进程
5 杀死进程
5.1 准备工作
没有完美的软件,杀死一个进程总是有必要的。
- 可以使用terminate方法立即杀死一个进程
- 可以使用is_alive方法看进程是否活着
5.2 实现
import multiprocessing
import time
def foo():
print('starting function')
for i in range(0,10):
print('-->%d\n' % i)
time.sleep(1)
print('Finished function')
if __name__ == '__main__':
p = multiprocessing.Process(target=foo)
print('Process before execution:', p, p.is_alive())
p.start()
print('Process running:', p, p.is_alive())
p.terminate()
print('Process terminated:', p, p.is_alive())
p.join()
print('Process joined:', p, p.is_alive())
print('Process exit code:', p.exitcode)
5.3 输出
Process before execution: <Process name='Process-1' parent=13404 initial> False
Process running: <Process name='Process-1' pid=2592 parent=13404 started> True
Process terminated: <Process name='Process-1' pid=2592 parent=13404 started> True
Process joined: <Process name='Process-1' pid=2592 parent=13404 stopped exitcode=-SIGTERM> False
Process exit code: -15
- 做的事情:创建一个进程,然后杀死它,等待结束后,查看进程的exit code
- exit code的规则
- == 0 :没有产生任何错误
-
0 :进程有一个错误,并退出这个代码
- < 0 :进程由一个-1 * ExitCode信号杀死,这个信号是一个正值
6 子类中定义进程
6.1 准备工作
为了实现一个multiprocessing定制子类,需要完成以下工作:
- 定义multiprocessing.Process的一个子类,重定义run()方法。
- 覆盖__init__(self[, args])方法来增加额外的参数(如果需要)。
- 覆盖run(self[,args])方法来实现启动进程时Process需要做的工作。
创建Process子类后,可以创建一个实例,然后调用start方法,会自动调用其run方法。
6.2 实现
import multiprocessing
class MyProcess(multiprocessing.Process):
def run(self):
print('called run method by %s' % self.name)
if __name__ == '__main__':
for i in range(10):
process = MyProcess()
process.start()
process.join()
6.3 输出
called run method by MyProcess-1
called run method by MyProcess-2
called run method by MyProcess-3
called run method by MyProcess-4
called run method by MyProcess-5
called run method by MyProcess-6
called run method by MyProcess-7
called run method by MyProcess-8
called run method by MyProcess-9
called run method by MyProcess-10
- 可以实现multiprocessing.Process的子类,覆盖__init__方法和run方法
7 使用队列交换数据
队列是一种先进先出(FIFO,First In First Out)的数据结构,就像排队一样。
7.1 准备工作
以经典的生产者/消费者问题来练习。生产者生产产品,消费者消费产品,有一个公共的缓冲区用来存放产品。需要实现生产者/消费者对缓冲区的互斥访问和先生产后消费的同步。这里就不着重讲这个问题了,这是操作系统的一个经典问题。
7.2 实现
这里的实现并没有涉及信号量,按理说要实现对队列的互斥访问。
import multiprocessing
import random
import time
class producer(multiprocessing.Process):
def __init__(self, queue):
multiprocessing.Process.__init__(self)
self.queue = queue
def run(self):
for i in range(5):
item = random.randint(0,256)
self.queue.put(item)
print('Process Producer : item %d appended to queue by %s' % (item, self.name))
time.sleep(1)
print('The size of queue is %s' % self.queue.qsize())
class consumer(multiprocessing.Process):
def __init__(self, queue):
multiprocessing.Process.__init__(self)
self.queue = queue
def run(self):
while True:
if self.queue.empty():
print('the queue is empty')
break
else:
time.sleep(2)
item = self.queue.get()
print('Process Consumer : item %d popped from queue by %s\n' % (item, self.name))
time.sleep(1)
if __name__ == '__main__':
queue = multiprocessing.Queue()
process_producer = producer(queue)
process_consumer = consumer(queue)
process_producer.start()
time.sleep(1)
process_consumer.start()
process_producer.join()
process_consumer.join()
7.3 输出
Process Producer : item 23 appended to queue by producer-1
The size of queue is 1
Process Producer : item 18 appended to queue by producer-1
The size of queue is 2
Process Producer : item 0 appended to queue by producer-1
Process Consumer : item 23 popped from queue by consumer-2
The size of queue is 2
Process Producer : item 3 appended to queue by producer-1
The size of queue is 3
Process Producer : item 36 appended to queue by producer-1
The size of queue is 4
Process Consumer : item 18 popped from queue by consumer-2
Process Consumer : item 0 popped from queue by consumer-2
Process Consumer : item 3 popped from queue by consumer-2
Process Consumer : item 36 popped from queue by consumer-2
the queue is empty
- 使用multiprocessing.Queue()来传递信号
- queue.put(item)入队
- queue.get(item)出队
8 使用管道交换对象
8.1 准备工作
管道(pipe)是什么?管道连接两个进程,通过接收/发送来实现进程间通信。
- multiprocessing.Pipe(duplex)可以实例化一个管道,返回一个对象对(conn1,conn2),表示管道的两端。
- duplex = True表示管道双向
- duplex = False表示管道单向,conn1只用于接收,conn2只用于发送
8.2 实现
进程1创建item 0~9数字送入Pipe1,进程2从Pipe1接收数字,将其平方后送入Pipe2。最后从Pipe2接收。
import multiprocessing
from venv import create
def create_items(pipe):
output_pip, _ = pipe
for item in range(10):
output_pip.send(item)
output_pip.close()
def multiply_items(pipe_1, pipe_2):
close, input_pip = pipe_1
close.close()
output_pipe, _ = pipe_2
try:
while True:
item = input_pip.recv()
output_pipe.send(item*item) # 返回各个管道元素的乘积,即平方
except EOFError:
output_pipe.close()
if __name__ == '__main__':
pipe_1 = multiprocessing.Pipe(True)
process_pipe_1 = multiprocessing.Process(target=create_items, args=(pipe_1,))
process_pipe_1.start()
# 进程1创建0~9的数字,送入pipe1
pipe_2 = multiprocessing.Pipe(True)
process_pipe_2 = multiprocessing.Process(target=multiply_items, args=(pipe_1,pipe_2,))
process_pipe_2.start()
# 进程2接收pipe1的数字,并将其平方后送入pipe2
# 关闭两个管道
pipe_1[0].close()
pipe_2[0].close()
# 打印结果
try:
while True:
print(pipe_2[1].recv())
except EOFError:
print("End")
8.3 输出
0
1
4
9
16
25
36
49
64
81
End
- 为什么conn需要调用close?因为如果不这样的话,recv的连接就会一致阻塞,等待接收,不会出现EOF。
- 使用Pipe会比Queue更快,因为Queue建立在Pipe之上。Queue常用于多进程间的通信。
9 同步进程
为什么需要同步?因为多个进程一起工作时,有时需要严格保证执行顺序,否则会造成错误或无法预知的结果。
进程的同步原语与threading库中的同步原语非常相似。如下:
- Lock:这个对象可以是锁定或非锁定状态。Lock对象有两个方法acquire()和release()来管理对同一个共享资源的访问。
- Event:用来实现进程间的简单通信;一个进程通知一个事件,另一个进程等待这个通知。Event对象有两个方法set()和clear()来管理它自己的内部标志。
- Condition:用来同步一个工作流的各部分。它有两个基本方法:wait()用来等待一个条件,notify_all()用来通知所应用的条件。
- Semaphore:用来共享一个公共资源,例如可以同时支持固定数目的连接。
- RLock:这定义了重入锁对象。
- Barrier:这个对象将程序划分为阶段,要求所有进程都达到屏障才能继续。屏障之后执行的代码不能与屏障之前的代码并发运行。
9.1 准备工作
Python中的屏障(Barrier)用来等待固定数目的进程执行完成,然后给定的进程才能继续执行。这里是一个用屏障实现同步的例子。
9.2 实现
# 用barrier来实现进程同步
import multiprocessing
from multiprocessing import Barrier, Lock, Process
from time import time
from datetime import datetime
def test_with_barrier(synchronizer, serializer):
name = multiprocessing.current_process().name
synchronizer.wait()
now = time()
with serializer:
print('process %s ----> %s' % (name, datetime.fromtimestamp(now)))
def test_without_barrier():
name = multiprocessing.current_process().name
now = time()
print('process %s ----> %s' % (name, datetime.fromtimestamp(now)))
if __name__ == '__main__':
synchronizer = Barrier(2)
serializer = Lock()
Process(name='p1 - test_with_barrier', target=test_with_barrier, args=(synchronizer, serializer,)).start()
Process(name='p2 - test_with_barrier', target=test_with_barrier, args=(synchronizer, serializer,)).start()
Process(name='p3 - test_without_barrier', target=test_without_barrier).start()
Process(name='p4 - test_without_barrier', target=test_without_barrier).start()
9.3 输出
process p3 - test_without_barrier ----> 2022-01-21 14:10:45.443280
process p4 - test_without_barrier ----> 2022-01-21 14:10:45.453279
process p2 - test_with_barrier ----> 2022-01-21 14:10:45.473277
process p1 - test_with_barrier ----> 2022-01-21 14:10:45.473277
在9.2的代码中,Barrier实现了这样的功能:等两个进程都到达指定位置,然后一起继续前进,所以p1和p2才打印了相同的时间戳。
10 使用进程池
利用进程池机制,在多个输入值上执行的一个函数可以并行化,将输入数据分布到多个进程,实现数据级并行(data parallelism)。
10.1 准备工作
multiprocessing.Pool类可以完成简单的并行处理任务。
Pool类有以下方法:
- apply(): 这会阻塞,直到结果就绪。
- apply_async():apply()的一个变体,实现了异步,即主进程不会等待所有子进程运行结束。
- map():内置map函数的并行版本。这个方法会阻塞,直到结果就绪,它将可迭代处理的数据划分为多个块,作为单独的任务提交到进程池。
- map_async():这是map()的一个变形,会返回一个结果对象。如果指定了回调,应当可以调用这个回调,这要接受一个参数。结果就绪时,会调用一个回调。回调应当立即完成,否则处理结果的进程会阻塞。
10.2 实现
import multiprocessing
import time
def function_square(data):
x = data*data
x = 2*x+9
x = x*x
x = x-1000
x = x*6
x = x*x
x = x/78
x = x*2.98
x = x*0.492
return x
if __name__ == '__main__':
inputs = list(range(0,1000000))
pool = multiprocessing.Pool(processes=8)
t0 = time.time()
pool_outputs = pool.map(function_square, inputs)
t1 = time.time()
t2 = time.time()
outputs = list(map(function_square, inputs))
#time.sleep(1)
t3 = time.time()
pool.close()
pool.join()
print('pool: %.12f | no_pool: %.12f' % (t1-t0, t3-t2))
10.3 输出
pool: 0.280999183655 | no_pool: 0.480020523071
总结一下就是多进程地进行map,然而额外开销很大,简单运算的情况下还是直接map要快得多。