Python 实现线程式编程非常简单,但是这种方法的一个缺陷就是它并不总是能够提高应用程序的速度,因为全局解释器锁(Global Interpreter Lock,GIL)将线程有效地限制到一个核中。如果需要使用计算机中的所有核,那么通常都需通过 对 经常使用 fork 操作来实现,从而提高速度。处理进程组是件困难的事情,因为为了在进程之间进行通信,需要对所有调用进行协调,这通常会使事情变得更复杂。
幸运的是,自 2.6 版本起,Python 包括了一个名为 “多进程(multiprocessing)” 的模块来帮助处理进程。该进程模块的 API 与线程 API 的工作方式有些相似点,但是也存在一些需要特别注意的不同之处。主要区别之一就是进程拥有的一些微妙的底层行为,这是高级 API 永远无法完全抽象出来的。可以从多进程模块的官方文档中了解有关这方面内容(参见 参考资料 小节)。
进程和线程在并发性的工作原理方面存在一些明显的差异。通过阅读我撰写的有关线程的 developerWorks 文章,可以进一步了解这些差异(参见 参考资料)。在进程执行 fork 时,操作系统将创建具有新进程 ID 的新的子进程,复制父进程的状态(内存、环境变量等)。首先,在我们实际使用进程模块之前,先看一下 Python 中的一个非常基本的 fork 操作。
- #!/usr/bin/env python
- """A basic fork in action"""
- import os
- def my_fork():
- child_pid = os.fork()
- if child_pid == 0:
- print "Child Process: PID# %s" % os.getpid()
- else:
- print "Parent Process: PID# %s" % os.getpid()
- if __name__ == "__main__":
- my_fork()
现在来看一下以上代码的输出:
- mac% python fork.py
- Parent Process: PID# 5285
- Child Process: PID# 5286
在下一个示例中,增强初始 fork 的代码,并设置一个环境变量。该环境变量随后将被复制到子进程中。下面给出了相应的代码:
- #!/usr/bin/env python
- """A fork that demonstrates a copied environment"""
- import os
- from os import environ
- def my_fork():
- environ['FOO']="baz"
- print "FOO environmental variable set to: %s" % environ['FOO']
- environ['FOO']="bar"
- print "FOO environmental variable changed to: %s" % environ['FOO']
- child_pid = os.fork()
- if child_pid == 0:
- print "Child Process: PID# %s" % os.getpid()
- print "Child FOO environmental variable == %s" % environ['FOO']
- else:
- print "Parent Process: PID# %s" % os.getpid()
- print "Parent FOO environmental variable == %s" % environ['FOO']
- if __name__ == "__main__":
- my_fork()
下面给出了 fork 的输出:
- mac% python env_fork.py
- FOO environmental variable set to: baz
- FOO environmental variable changed to: bar
- Parent Process: PID# 5333
- Parent FOO environmental variable == bar
- Child Process: PID# 5334
- Child FOO environmental variable == bar
在输出中,可以看到 “修改后的” 环境变量 FOO 留在了子进程和父进程中。您可以通过在父进程中再次修改环境变量来进一步测试这个示例,您将看到子进程现在是完全独立的,它有了自己的生命。注意,子进程 模块也可用于 fork 进程,但是实现方式没有多进程模块那么复杂。
现在您已经了解 Python fork 操作的基本知识,让我们通过一个简单例子了解它在更高级的多进程库中的使用。在这个示例中,仍然会出现 fork,但是已经为我们处理了大部分标准工作。
- #!/usr/bin/env python
- from multiprocessing import Process
- import os
- import time
- def sleeper(name, seconds):
- print 'starting child process with id: ', os.getpid()
- print 'parent process:', os.getppid()
- print 'sleeping for %s ' % seconds
- time.sleep(seconds)
- print "Done sleeping"
- if __name__ == '__main__':
- print "in parent process (id %s)" % os.getpid()
- p = Process(target=sleeper, args=('bob', 5))
- p.start()
- print "in parent process after child process start"
- print "parent process about to join child process"
- p.join()
- print "in parent process after child process join"
- print "parent process exiting with id ", os.getpid()
- print "The parent's parent process:", os.getppid()
如果查看输出,将会看到下面的内容:
- mac% python simple.py
- in parent process (id 5245)
- in parent process after child process start
- parent process about to join child process
- starting child process with id: 5246
- parent process: 5245
- sleeping for 5
- Done sleeping
- in parent process after child process join
- parent process exiting with id 5245
- The parent's parent process: 5231
可以看到从主进程分出了一个子进程,该子进程随后休眠了 5 秒种。子进程分配是在调用 p.start()
时发生的。在下一节中,您将看到这个基础的程序将扩展为更大的程序。
- #!/usr/bin/python
- import os, sys, time, datetime, traceback;
- from multiprocessing import Process;
- nodes = ('master', 'slave1', 'slave5', 'slave17');
- cmd = 'ssh %s "/xxx_project/download/dump_data.sh %s"'
- def download(node, exec_cmd):
- try:
- print "[INFO]:Download started on node:",node;
- print("[INFO]:" + exec_cmd);
- if os.system(exec_cmd) != 0:
- print("[ERROR]:" + exec_cmd);
- print "[INFO]:Download finished on node:",node;
- except Exception as e: #table doesn't exist probably
- print e;
- traceback.print_exc();
- if __name__ == "__main__":
- print("%s: controller started." % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),));
- date_str = ''
- if len(sys.argv) > 1: date_str = sys.argv[1];
- plist = [];
- for node in nodes:
- exec_cmd = cmd % (node, date_str);
- p = Process(target = download, args = (node, exec_cmd));
- plist.append(p);
- p.start();
- for p in plist:
- p.join();
- print("%s: controller finished." % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()),));
上面的代码实现了一个多进程执行命令的控制器,它根据获取的节点参数,创建多个进程,ssh到节点上并执行命令。
进程实际上是在p.start的时候真正创建并且开始运行的。调用p.join就跟线程一样,会等待进程结束。