Python多进程编程详解

2,647 阅读7分钟

本文代码在 Python 3.6 环境下测试通过。

简介

多进程(multiprocessing)模块是在 Python 2.6 版本中加入的,和多线程(threading)模块类似,都是用来做并行运算的。不过Python既然有了threading,为什么还要搞一个multiprocessing呢?这是因为Python内部有一个全局解释锁(GIL),任何一个进程任何时候只允许一个线程进行CPU运算,如果一个进程中的某个线程在进行CPU运算时获得GIL,其他线程将无法进行CPU运算只能等待,使得多线程无法利用CPU多核的特性。多进程处理实际上对每个任务都会生成一个操作系统的进程,并且每一个进程都被单独赋予了Python的解释器和GIL,所以程序在运行中有多个GIL存在,每个运行者的线程都会拿到一个GIL,在不同的环境下运行,自然也可以被分配到不同的处理器上。

创建进程(Process)

multiprocessing模块提供了一个Process类可以创建进程对象。创建进程有两种方式,第一种通过Process类直接创建,参数target指定子进程要执行的程序。第二种通过继承Process类来实现。

我们先用第一种方式创建子进程,子进程会将传递给它的参数扩大一倍,代码如下:

#-*- coding:utf8 -*-
import os
from multiprocessing import Process, current_process

def doubler(number):
    result = number * 2
    # 获取子进程ID
    proc_id = os.getpid()
    # 获取子进程名称
    proc_name = current_process().name
    print('proc_id:{0} proc_name:{1} result:{2}'.format(proc_id, proc_name, result))

if __name__ == '__main__':
    numbers = [5, 10, 15, 20, 25]
    procs = []	
    # 父进程ID和名称
    print('parent_proc_id:{0} parent_proc_name:{1}'.format(os.getpid(), current_process().name))
    
    for num in numbers:
    	# 创建子进程
    	proc = Process(target=doubler, args=(num,))
    	procs.append(proc)
    	# 启动子进程
    	proc.start()
    
    # join方法会让父进程等待子进程结束后再执行
    for proc in procs:
    	proc.join()
    
    print("Done.")

第二种方式通过继承Process类,并重写run方法:

class MyProcess(Process):
    def __init__(self, number):
    	# 必须调用父类的init方法
    	super(MyProcess, self).__init__()
    	self.number = number
    
    def run(self):
    	result = self.number * 2
    	# 获取子进程ID
    	# self.pid
    	proc_id = os.getpid()
    	# 获取子进程名称
    	# self.name
    	proc_name = current_process().name
    	print('proc_id:{0} proc_name:{1} result:{2}'.format(proc_id, proc_name, result))

if __name__ == '__main__':
    numbers = [5, 10, 15, 20, 25]
    procs = []
    # 父进程的ID和名称
    print('parent_proc_id:{0} parent_proc_name:{1}'.format(os.getpid(), current_process().name))
    
    for num in numbers:
    	# 创建子进程
    	proc = MyProcess(num)
    	procs.append(proc)
    	# 启动子进程,启动一个新进程实际就是执行本进程对应的run方法
    	proc.start()
    
    # join方法会让父进程等待子进程结束后再执行
    for proc in procs:
    	proc.join()
    
    print("Done.")

进程锁(Lock)

multiprocessing模块和threading模块一样也支持锁。通过acquire获取锁,执行操作后通过release释放锁。

#-*- coding:utf8 -*-
from multiprocessing import Process, Lock

def printer(item, lock):
    # 获取锁
    lock.acquire()
    try:
    	print(item)
    except Exception as e:
    	print(e)
    else:
    	print('no exception.')
    finally:
        # 释放锁
    	lock.release()

if __name__ == '__main__':
    # 实例化全局锁
    lock = Lock()
    items = ['PHP', 'Python', 'Java']
    procs = []
    
    for item in items:
    	proc = Process(target=printer, args=(item, lock))
    	procs.append(proc)
    	proc.start()
    
    for proc in procs:
    	proc.join()
    
    print('Done.')

进程池(Pool)

Pool类表示工作进程的池子,它可以提供指定数量的进程供用户调用,当有请求提交到进程池时,如果进程池有空闲进程或进程数还没到达指定上限,就会分配一个进程响应请求,否则请求只能等待。Pool类主要在执行目标多且需要控制进程数量的情况下使用,如果目标少且不用控制进程数量可以使用Process类。

进程池可以通过map和apply_async方法来调用执行代码,首先我们来看map方法:

#-*- coding:utf8 -*-
import os
from multiprocessing import Pool, current_process

def doubler(number):
    result = number * 2
    proc_id = os.getpid()
    proc_name = current_process().name
    print('proc_id:{0} proc_name:{1} result:{2}'.format(proc_id, proc_name, result))

if __name__ == '__main__':
    numbers = [5, 10, 15, 20, 25]
    pool = Pool(processes=3)
    pool.map(doubler, numbers)
    
    # 关闭pool使其不再接受新的任务
    pool.close()
    
    # 关闭pool,结束工作进程,不在处理未完成的任务
    # pool.terminate()
    
    # 主进程阻塞,结束工作进程,不再处理未完成的任务,join方法要在close或terminate之后使用
    pool.join()
    
    print('Done')

map只能向处理函数传递一个参数。

下面来看一下apply/apply_async函数,apply函数是阻塞的,apply_async函数是阻塞的,这里我们以apply_async函数为例:

#-*- coding:utf8 -*-
import os, time
from multiprocessing import Pool, current_process

def doubler(number, parent_proc_id, parent_proc_name):
    result = number * 2
    proc_id = os.getpid()
    proc_name = current_process().name
    # 设置等待时间,可以验证apply和apply_async的阻塞和非阻塞
    time.sleep(2)
    print('parent_proc_id:{0} parent_proc_name:{1} proc_id:{2} proc_name:{3} number:{4} result:{5}'.format(parent_proc_id, parent_proc_name, proc_id, proc_name, number, result))

if __name__ == '__main__':
    numbers = [5, 10, 15, 20, 25]
    parent_proc_id = os.getpid()
    parent_proc_name = current_process().name
    pool = Pool(processes=3)
    for num in numbers:
    	# 非阻塞
    	pool.apply_async(doubler, (num, parent_proc_id, parent_proc_name))
    	# 阻塞其它进程
    	# pool.apply_async(doubler, (num, parent_proc_id, parent_proc_name))
    	
    # 关闭pool使其不再接受新的任务
    pool.close()
    
    # 关闭pool,结束工作进程,不在处理未完成的任务
    # pool.terminate()
    
    # 主进程阻塞,结束工作进程,不再处理未完成的任务,join方法要在close或terminate之后使用
    pool.join()
    
    print('Done')

进程间通信

进程间通信的方式一般有管道(Pipe)、信号(Signal)、消息队列(Message)、信号量(Semaphore)、共享内存(Shared Memory)、套接字(Socket)等。这里我们着重讲一下在Python多进程编程中常用的进程方式multiprocessing.Pipe函数和multiprocessing.Queue类。

Pipe

multiprocessing.Pipe()即管道模式,调用Pipe()方法返回管道的两端的Connection。Pipe方法返回(conn1, conn2)代表一个管道的两个端。Pipe方法有duplex参数,如果duplex参数为True(默认值),那么这个管道是全双工模式,也就是说conn1和conn2均可收发;duplex为False,conn1只负责接受消息,conn2只负责发送消息。send()和recv()方法分别是发送和接受消息的方法。一个进程从Pipe某一端输入对象,然后被Pipe另一端的进程接收,单向管道只允许管道一端的进程输入另一端的进程接收,不可以反向通信;而双向管道则允许从两端输入和从两端接收。

#-*- coding:utf8 -*-
import os, time
from multiprocessing import Process, Pipe, current_process

def proc1(pipe, data):
    for msg in range(1, 6):
        print('{0} 发送 {1}'.format(current_process().name, msg))
        pipe.send(msg)
        time.sleep(1)
    pipe.close()

def proc2(pipe, length):
    count = 0
    while True:
        count += 1
        if count == length:
            pipe.close()
        try:
            # 如果没有接收到数据recv会一直阻塞,如果管道被关闭,recv方法会抛出EOFError
            msg = pipe.recv()
            print('{0} 接收到 {1}'.format(current_process().name, msg))
        except Exception as e:
            print(e)
            break

if __name__ == '__main__':
    conn1, conn2 = Pipe(True)
    data = range(0, 6)
    length = len(data)
    proc1 = Process(target=proc1, args=(conn1, data))
    proc2 = Process(target=proc2, args=(conn2, length))
    
    proc1.start()
    proc2.start()

    proc1.join()
    proc2.join()
    
    print('Done.')

Queue

Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。Queue的使用主要是一边put(),一边get(),但是Queue可以是多个Process进行put()操作,也可以是多个Process进行get()操作。

  • put方法用来插入数据到队列中,put方法还有两个可选参数:block和timeout。如果block为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果block为False,但该Queue已满,会立即抛出Queue.Full异常。
  • get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:block和timeout。如果block为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果block为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值;否则,如果队列为空,则立即抛出Queue.Empty异常。

在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

#-*- coding:utf8 -*-
import os, time, random
from multiprocessing import Process, Queue

def write(q):
    print('Process to write: %s' % os.getpid())
    for val in range(0, 6):
        print('Put %s to queue...' % val)
        q.put(val)
        time.sleep(random.random())

def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        try:
            val = q.get(block=True, timeout=5)
            print('Get %s from queue.' % val)
        except Exception as e:
            if q.empty():
                print('队列消费完毕.')
                break
                
if __name__ == '__main__':
    q = Queue()
    
    proc1 = Process(target=write, args=(q,))
    proc2 = Process(target=read, args=(q,))

    proc1.start()
    proc2.start()

    proc1.join()
    proc2.join()
    
    # 如果proc2不break的话会一直阻塞,不调用join调用terminate方法可以终止进程
    # proc2.terminate()
    
    print('Done.')

Pipe的读写效率要高于Queue。那么我们如何的选择它们呢?

  • 如果你的环境是多生产者和消费者,那么你只能是选择queue队列。
  • 如果两个进程间处理的逻辑简单,但是就是要求绝对的速度,那么pipe是个好选择。