1. 为什么使用python多进程?
因为Python使用全局解释器锁(GIL),它会将进程中的线程序列化,导致每个进程中最多同时运行一个线程,也就是Python多线程不能改善程序性能,不能发挥多核CPU并行提高运行速度的目的。而使用多进程则不受限制,所以实际应用中都是推荐多进程的。
如果执行每个子进程消耗的时间非常短(如执行+1操作),那么就不必使用多进程,因为进程的启动关闭也会耗费系统资源。
使用多进程往往是用于处理CPU密集型(科学计算)的任务需求,如果是IO密集型的任务(如文件读取,爬虫等),应该使用多线程处理。
2. multiprocessing常用组件及功能
创建管理进程的模块:
Process(用于创建进程)
Pool(用于创建管理进程池)
Queue(用于进程通信,资源共享)
Value,Array(用于进程通信,资源共享)
Pipe(用于管道通信)
Manager(用于资源共享)
同步子进程的模块:
Condition
Event(用于实现进程间同步通信)
Lock(当多个进程需要访问共享资源时,Lock用于避免访问冲突)
RLock
Semaphore(用于控制访问共享资源的数量)
3. 进程
3.1 创建子进程Process
一个进程应该用来做什么?它应该保存哪些状态?它的生命周期是什么样的?
一个进程需要处理一些不同的任务,或者处理不同的对象。
创建子进程需要一个函数和相关参数,参数可以是Process(target = func, args = ( ), kwargs = { }, name =“a”),target选定子进程的函数,args传入函数的参数,name用来标识子进程。
控制子进程进入不同阶段的是方法.start( ),.join( ),.is_alive( ),.terminate( ),.exitcode,这些方法只能在创建子进程中执行。
来看一段代码
from multiprocessing import Process, current_process
# 定义一个函数
def func():
time.sleep(1)
proc = current_process()
print(proc.name, proc.pid)
# 创建子进程,选定func为子进程函数,参数为空
sub_proc = Process(target = func, args = (), name = 'SubProcess')
sub_proc.start()
sub_proc.join()
# 继续执行主进程
proc = current_process()
print(proc.name, proc.pid)
'''
out:
SubProcess 16977
MainProcess 16384
'''
上述是在主进程中创建子进程,然后用.start( )启动子进程,用.join( )等待子进程执行完成,接着继续执行主进程。另一个在主进程中,运行子进程的例子:
from multiprocessing import Process
import os
def run_proc(name):
# 多个变量的字符串转义,需要把变量放到一个tuple中
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__ == '__main__':
print('Parent process %s.' % os.getpid())
# 创建子进程,target是子进程函数,args是子进程函数的参数
# 参数要写成tuple形式
p = Process(target = run_proc, args = ('test',))
p.start()
p.join()
print('End')
'''
out:
Parent process 16384.
Run child process test (16978)...
End
MainProcess 16384
'''
3.2 使用subprocess创建并控制子进程
import subprocess
# 在命令行下执行一个命令
print('$ nslookup')
# 命令的子进程
p = subprocess.Popen(['nslookup'],
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE)
output, err = p.communicate(b'set q = mx\npython.org\nexit\n')
print(err.decode('utf-8'))
print(output.decode('utf-8')) # 把输出解码成utf-8
print('Exit code:', p.returncode)
4. 进程同步
Lock
锁是为了确保数据一致性,比如读写锁,每个进程给一个变量增加1,如果在一个进程读取但还没有写入时,另外的进程同时读取并写入该值,则最后写入的值是错误的,这时候就需要锁。
from multiprocessing import Process, Lock
def func(lock):
lock.acquire() # 加锁
# do mysql query select update ...
lock.release() # 解锁
lock = Lock()
# 设置4个进程,执行func
for i in range(4):
proc = Process(target = func, args = (lock,))
proc.start()
Semaphore
Semaphore和Lock稍有不同,Semaphore相当于N把锁,获取其中一把就可以执行了。信号量的总数N在构造时传入,s = Semaphore(N)。和Lock一样,如果信号量为0,则进程堵塞,直到信号大于0。
Pipe
Pipe是在两个进程之间通信的工具,Pipe构造器会返回两个端
conn1, conn2 = Pipe(True)
如果参数为True,则双端口都可接收发送(全双工的);如果参数为空,则前面的端口conn1用于接收,后面的端口conn2用于发送。代码如下:
from multiprocessing import Pipe, Process
# 发送函数
def proc1(pipe):
for i in range(10000):
pipe.send(i)
# 接收函数
def proc2(pipe):
while True:
print("proc2 rev:", pipe.recv())
pipe = Pipe()
# 创建进程,用于发送(pipe的第2个参数用于发送,pipe[1]是proc1的参数)
Process(target = proc1, args = (pipe[1],)).start()
# 创建进程,用于接收(pipe的第1个参数用于接收,pipe[0]是proc2的参数)
Process(target = proc2, args = (pipe[0],)).start()
'''
out:
None
proc2 rev: 0
proc2 rev: 1
proc2 rev: 2
proc2 rev: 3
proc2 rev: 4
...
...
...
proc2 rev: 9995
proc2 rev: 9996
proc2 rev: 9997
proc2 rev: 9998
proc2 rev: 9999
'''
Pipe的每个端口最多同时一个进程读写,否则数据会出各种问题。
队列Queues
multiprocessing.Queue与Queue.Queue非常相似。其API列表如下:
qsize()
empty()
full()
put()
put_nowait()
get()
get_nowait()
close()
join_thread()
cancel_join_thread()
当Queue为Queue.Full状态时,再次put()会堵塞;当状态为Queue.Empty时,再次get()也是。当设置了超时参数而超时,put( )或get( )会抛出异常。Queue主要用于多个进程产生和消费,下面展示十个生产者进程,一个消费者进程,共用同一个队列进行同步。
from multiprocessing import Process, Queue
def producer(q):
print('Process to producer: %s' % os.getpid())
for i in range(10):
q.put(i)
def consumer(q):
print('Process to consumer: %s' % os.getpid())
while True:
print("consumer", q.get())
q = Queue()
# 设置十个生产者进程
for i in range(10):
process_producer = Process(target = producer, args = (q,))
process_producer.start()
# 一个消费者进程
process_consumer = Process(target = consumer, args = (q,))
process_consumer.start()
利用同一个队列,同步读写,下面是代码内容:
from multiprocessing import Process, Queue
import os, time, random
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['A', 'B', 'C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('Get %s from queue.' % value)
if __name__=='__main__':
q = Queue()
pw = Process(target = write, args = (q,))
pr = Process(target = read, args = (q,))
pw.start()
pr.start()
pw.join()
pr.terminate()
Lock、Pipe、Queue 和Pipe需要注意,尽量避免使用Process.terminate来终止程序,否则将会导致很多问题。
5. 进程间数据共享
前一节中, Pipe、Queue都有一定的数据共享功能,但是它们会堵塞进程。而共享内存、Manager这两种数据共享方式不会堵塞进程, 而且都是多进程安全的。
5.1 共享内存Value,Array
共享内存有两个变量: Value, Array,这两个变量内部都实现了锁机制,因此是多进程安全的。用法如下:
from multiprocessing import Process, Value, Array
def func(n, a):
n.value = 50
for i in range(len(a)):
a[i] += 10
num = Value('d', 0.0)
ints = Array('i', range(10))
p = Process(target = func, args = (num, ints))
p.start()
p.join()
print(num.value)
print(ints[:])
'''
Out:
50.0
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
'''
Value和Array都需要设置其中存放值的数据类型,d是double类型,i是int类型,具体对应关系在Python标准库的sharedctypes模块中查看。
5.2 服务进程Manager
共享内存支持Value和Array两种结构, 这些值在主进程中管理很分散。在Python 中还有一统天下,无所不能的Server process,专门用于数据共享,其支持的类型非常多,比如list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Queue, Value,Array。用法如下:
from multiprocessing import Process, Manager
def func(dct, lst):
dct[88] = 88
lst.reverse()
manager = Manager()
dct = manager.dict()
lst = manager.list(range(5, 10))
p = Process(target = func, args = (dct, lst))
p.start()
p.join()
print(dct, '|', lst)
# Out: {88: 88} | [9, 8, 7, 6, 5]
一个Manager对象就是一个服务进程。在多进程程序中,推荐数据共享就用一个manager对象管理。
6. 进程管理
如果有50个任务要执行, 但是CPU只有4核, 你可以创建50个进程来做这个事情,但这样会增加管理成本。
如果你只想创建4个进程,让它们轮流完成任务,不用自己去管理具体进程的创建销毁,那Pool是非常有用的。
6.1 Pool进程池
Pool是能够管理一定数量进程的进程池。当有空闲进程时,Pool利用空闲进程完成任务,直到所有任务完成为止。使用方法如下:
from multiprocessing import Pool
def func(x):
return x*x
pool = Pool(processes = 4)
print(pool.map(func, range(8)))
# out: [0, 1, 4, 9, 16, 25, 36, 49],计算0~7自然数的平方
Pool进程池创建4个进程,不管有没有任务,都一直在进程池中等候,等到有数据的时候就开始执行。考虑到切换进程的时间成本,一般进程池进程个数设置为CPU的逻辑核数,最多不超过其200%。
6.2 Pool异步执行
Pool 的 API 列表如下:
apply(func[, args[, kwds]])
apply_async(func[, args[, kwds[, callback]]])
map(func, iterable[, chunksize])
map_async(func, iterable[, chunksize[, callback]])
imap(func, iterable[, chunksize])
imap_unordered(func, iterable[, chunksize])
close()
terminate()
join()
其中,apply_async( )和map_async( )执行之后,异步返回结果。使用方法如下:
- apply_async( )
from multiprocessing import Pool
import multiprocessing
import os
import random
import time
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4) # 进程池大小为4,即一次随机执行4个task
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
'''
out:
Parent process 17482.
Run task 1 (17863)...
Run task 0 (17862)...
Run task 2 (17864)...
Run task 3 (17865)...
Waiting for all subprocesses done...
Task 1 runs 0.22 seconds.
Run task 4 (17863)...
Task 2 runs 0.44 seconds.
Task 3 runs 0.94 seconds.
Task 0 runs 1.48 seconds.
Task 4 runs 2.21 seconds.
All subprocesses done.
'''
- map_async( )
from multiprocessing import Pool
def func(x):
return x*x
def callback(x):
print(x, 'in callback')
pool = Pool(processes = 4)
result = pool.map_async(func, range(8), 8, callback)
print(result.get(), 'in main')
'''
out:
[0, 1, 4, 9, 16, 25, 36, 49] in callback
[0, 1, 4, 9, 16, 25, 36, 49] in main
'''
有两个值得提到:一个是callback,另外一个是 multiprocessing.pool.AsyncResult,即上文的变量result。
callback是在结果返回之前调用的一个函数,这个函数必须只有一个参数,它会首先接收到结果。callback不能有耗时操作,因为它会阻塞主线程。
AsyncResult 是获取结果的对象,其API如下:
- get([timeout])
- wait([timeout])
- ready()
- successful()
get是获取结果,如果设置了timeout时间,超时会抛出multiprocessing.TimeoutError异常;wait是等待执行完成;ready测试是否已经完成;successful是在确定已经ready的情况下,如果执行中没有抛出异常,则成功,如果没有ready就调用该函数,会得到一个AssertionError 异常。
6.3 Pool管理流程
我们看看Pool的执行流程,有三个阶段:
一个进程池接收很多任务,然后分开执行任务
不再接收任务
等所有任务完成了,回家不干了
这就是上面的Pool API方法:close停止接收新的任务,如果还有任务来,就会抛出异常;join是等待所有任务完成,join必须要在close之后调用,否则会抛出异常;terminate非正常终止,当内存不够用时,垃圾回收器调用的就是这个方法。