要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识。
Unix/Linux操作系统提供了一个fork()
系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()
调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0
,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()
就可以拿到父进程的ID。
Python的os
模块封装了常见的系统调用,其中就包括fork
,可以在Python程序中轻松创建子进程:
1 # /usr/bin/python 2 import os 3 import time 4 5 6 def main(): 7 pid = os.fork() 8 if pid == 0: # 子进程中返回0 9 print("I am child process %d, my parent is %d" % (os.getpid(), os.getppid())) 10 time.sleep(1) 11 else: # 父进程中返回子进程id 12 print("I %d just created child %d" % (os.getpid(), pid)) 13 time.sleep(1) # 防止父进程提前结束,子进程将由init进程接管,导致子进程中的os。getppid()输出的进程id是1 14 15 if __name__ == '__main__': 16 main()
程序运行结果:
1 I 20981 just created child 20982 2 I am child process 20982, my parent is 20981
由于Windows没有fork
调用,上面的代码在Windows上无法运行。在Linux,Mac,Unix上都可以运行
有了fork
调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。
multiprocessing
如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork
调用,难道在Windows上无法用Python编写多进程的程序?
由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing
模块就是跨平台版本的多进程模块。
multiprocessing
模块提供了一个Process
类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:
(1)Process类:
1 import multiprocessing 2 import os 3 import time 4 5 6 # 子进程要执行的代码 7 def run_process(i): 8 print("%d Child %s process run" % (i, multiprocessing.current_process().name, )) 9 time.sleep(1) 10 print("%d Child %s process end" % (i, multiprocessing.current_process().name,)) 11 12 13 def main(): 14 print("Process %d run" % (os.getpid())) 15 p1 = multiprocessing.Process(target=run_process, args=(1,)) # 和多线程Thread类创建实例相似 16 p1.start() 17 p1.join() # 主进程等待子进程结束 18 print("Process %d stop" % (os.getpid())) 19 20 if __name__ == '__main__': 21 main()
执行结果如下:
1 Process 22324 run 2 1 Child Process-1 process run 3 1 Child Process-1 process end 4 Process 22324 stop
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process
实例,用start()
方法启动,这样创建进程比fork()
还要简单。
join()
方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
(2)Pool类:
在使用Python进行系统管理时,特别是同时操作多个文件目录或者远程控制多台主机,并行操作可以节约大量的时间。如果操作的对象数目不大时,还可以直接使用Process类动态的生成多个进程,十几个还好,但是如果上百个甚至更多,那手动去限制进程数量就显得特别的繁琐,此时Pool类就派上用场了。
Pool类可以提供指定数量的进程供用户调用,当有新的请求提交到Pool中时,如果池还没有满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求。
1 import multiprocessing 2 import os 3 import time 4 5 6 def run_process(i): 7 print("%d Child %s process run at %s" % (i, multiprocessing.current_process().name, time.time())) 8 time.sleep(1) 9 print("%d Child %s process end" % (i, multiprocessing.current_process().name,)) 10 11 12 def main(): 13 print("Process %d run" % (os.getpid())) 14 p2 = multiprocessing.Pool(multiprocessing.cpu_count()) 15 for i in range(5): 16 p2.apply_async(run_process, args=(i,)) # 该函数用于启动进程,传递不定参数,主进程是非阻塞且支持结果返回进行回调。 17 p2.close() # 关闭进程池(pool),使其不在接受新的任务。 18 p2.join() # 主进程阻塞等待子进程的退出,join方法必须在close或terminate之后使用。 19 print("Process %d stop" % (os.getpid())) 20 21 if __name__ == '__main__': 22 main()
运行结果:
1 Process 29676 run 2 0 Child SpawnPoolWorker-1 process run at 1487744098.910444 3 1 Child SpawnPoolWorker-3 process run at 1487744098.931447 4 2 Child SpawnPoolWorker-4 process run at 1487744098.936447 5 3 Child SpawnPoolWorker-2 process run at 1487744098.96145 6 0 Child SpawnPoolWorker-1 process end 7 4 Child SpawnPoolWorker-1 process run at 1487744099.911545 8 1 Child SpawnPoolWorker-3 process end 9 2 Child SpawnPoolWorker-4 process end 10 3 Child SpawnPoolWorker-2 process end 11 4 Child SpawnPoolWorker-1 process end 12 Process 29676 stop
代码解读:
对Pool
对象调用join()
方法会等待所有子进程执行完毕,调用join()
之前必须先调用close()
,调用close()
之后就不能继续添加新的Process
了。
请注意输出的结果,task 0
,1
,2
,3
是立刻执行的,而task 4
要等待前面某个task完成后才执行,这是因为Pool
的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool
有意设计的限制,并不是操作系统的限制。如果改成:
p2 = multiprocessing.Pool(5)
就可以同时跑5个进程。
由于Pool
的默认大小是CPU的核数,如果你的电脑拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。
pool类有一个map方法: def map(self, func, iterable, chunksize=None) 与内置的map函数用法行为基本一致,它会使进程阻塞直到返回结果:
1 def square(n): # 计算平方值 2 time.sleep(1) #计算一次休眠1s 3 print(n*n,time.time()) 4 return n*n 5 6 7 def main(): 8 print("Process %d run" % (os.getpid())) 9 number_list = [1, 2, 3, 4, 5, 6] 10 p2 = multiprocessing.Pool(multiprocessing.cpu_count()) # 本机4核CPU 11 p2.map(square, number_list) 12 print("Process %d stop" % (os.getpid())) 13 14 if __name__ == '__main__': 15 main()
运行结果:
1 Process 12264 run 2 1 1487744750.823629 3 4 1487744750.82863 4 9 1487744750.860633 5 16 1487744750.873634 6 25 1487744751.823729 7 36 1487744751.82873 8 Process 12264 stop
因为列表中共有6个元素,由于本机CPU有四核,在4个进程内的map方法同时可以对4个元素求平方,所以对于6个元素的列表,程序耗时2s。
由于map方法会使主进程阻塞,直到子进程返回,我们并没有调用p2.join(),主进程还是等待子进程结束