Python3多进程
2018-12-11 20:47:34

由于这两天自己负责的数据处理模块,都涉及到IO密集的数据读写,为了能够加快处理速度和调试速度,就想着使用Python的多进程,顺便借此机会,系统地学习一下Python的多进程。

多进程主要由以下几种方式:

  1. fork仅支持Unix/Linux系统

  2. multiprocessing(跨平台多进程)

    • Pool

    • Process

依次来看下这几种方式创建和实现多进程。

1. fork

fork其实是操作系统提供的API,操作系统从当前进程(父进程)创建(说“复制”可能更恰当)一个子进程。fork函数会从父进程和子进程分别都返回一次。

简单的看一个例子

1
2
3
4
5
6
7
8
9
10
11
# 代码是廖雪峰博客的代码
import os
print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
print(pid, type(pid))

if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

这段代码的输出结果如下:

1
2
3
4
5
Process (18838) start...
18841 <class 'int'>
I (18838) just created a child process (18841).
0 <class 'int'>
I am child process (18841) and my parent is 18838.

根据输出,来简单地理解下fork的基本流程。

第1行:是当前进程(即父进程的输出提示)。

第2,3行:结合两个输出提示一起看,不难理解os.fork()返回的pid=18841是其新复制出来的子进程的进程号。此时,正在执行的是父进程。

第4,5行:此时输出pid=0,且正在执行的是子进程。

可以理解成,从os.fork()往后,我们的进程复制了一份(包括fork后面的代码),变成了两个进程。

两个进程同时在执行,只不过在子进程中,fork返回的永远是0,父进程中返回的时子进程的ID。

因此可以这样通过简单判断的返回的结果,来区别子进程和父进程不同的工作。

1
2
3
4
5
6
7
8
9
import os
print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()

if pid == 0:
sub_process_job() # 子进程的任务
else:
current_process_job() # 父进程的任务

但是这种方法不好的地方,主要存在于限制了代码运行平台,对于python这种跨平台的语言来说,显然是不合适的。

2. multiprocessing.Process

multiprocessing模块提供一个Process类来代表一个进程对象。下面的进程主要演示启动一个子进程并等待其结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 代码来自廖雪峰博客
from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')

输出结果如下:

1
2
3
4
Parent process 22676.
Child process will start.
Run child process test (22686)...
Child process end.

这个过程就比之前的fork创建进程更加清晰,也更容易理解。只需要在创建进程时,传递目标函数和参数即可。

其中,进程的join()方法,我理解的是,当前进程(父进程)让子进程加入到自己的执行过程中,也就是父进程先等待子进程运行结束后,再继续往下执行。主要用于进程间的同步

3. multiprocessing.Pool()

multiprocessing还提供进程池,可以批量启动大量子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 代码来自廖雪峰博客
from multiprocessing import Pool
import os, time, random

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)
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.')

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Parent process 24058.
Waiting for all subprocesses done...
Run task 0 (24064)...
Run task 2 (24066)...
Run task 1 (24065)...
Run task 3 (24067)...
Task 2 runs 0.68 seconds.
Run task 4 (24066)...
Task 4 runs 0.15 seconds.
Task 1 runs 1.16 seconds.
Task 0 runs 2.18 seconds.
Task 3 runs 2.47 seconds.
All subprocesses done.

因为本人的机子是双核的(虚拟化后是四核),所以可以一次性批量启动4个进程。第5个进程需要等待其中任意一个进程执行结束之后,才能启动。

p.join()的方法会等待线程池中所有的子进程都结束后,才会继续执行。

插入个小插曲,在我之前做的一个小任务中,需要读取大量CSV文件,并将数据导入到数据库,一开始选择了将文件列表尽量平均的分割成多个子列表分别分配给各个子进程。最后导致代码一开始跑得飞快,但是越到后面越慢,甚至还出现多余残留的CSV文件。

后来,知道了线程池类中提供了map方法,可以理解成多进程的map。使用方法,与python内置的map是相同的。

上面那个小例子也可以改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from multiprocessing import Pool
import os, time, random

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)
# for i in range(5):
# p.apply_async(long_time_task, args=(i,))
p.map(long_time_task, [i for i in range(5)])
# print('Waiting for all subprocesses done...')
# p.close()
# p.join()
print('All subprocesses done.')

输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Parent process 30834.
Run task 0 (30835)...
Run task 1 (30836)...
Run task 2 (30837)...
Run task 3 (30838)...
Task 2 runs 0.78 seconds.
Run task 4 (30837)...
Task 0 runs 0.88 seconds.
Task 4 runs 0.12 seconds.
Task 3 runs 0.99 seconds.
Task 1 runs 1.41 seconds.
All subprocesses done.

结果也是相同的。

感觉这样的风格,更加pythonic。

进程间通信

在自己的第二个任务中,还涉及到多个进程想要访问同一个内存空间。但是程序却老出错,或者跑不动。

在Python中,多个进程之间通信,是需要专门的类和方式去实现的。

multiprocessing模块中封装了很多类去实现数据交换,常用的如QueuePipe。下面就是用Queue实现一个简单的读写队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 代码来自廖雪峰博客
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__':
# 父进程创建Queue,并传给各个子进程:
q = Queue()
pw = Process(target=write, args=(q,))
pr = Process(target=read, args=(q,))
# 启动子进程pw,写入:
pw.start()
# 启动子进程pr,读取:
pr.start()
# 等待pw结束:
pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止:
pr.terminate()

参考

多进程
理解Python并发编程一篇就够了 - 进程篇