多进程编程知识是 Python 程序员进阶高级的必备知识点, 我们平时习惯了使用 multiprocessing 库来操纵多进程, 但是并不知道它的具体实现原理. 下面我对多进程的常用知识点都简单列了一遍, 使用原生的多进程方法调用, 帮助读者理解多进程的实现机制. 代码跑在 linux 环境下. 没有 linux 条件的, 可以使用 docker 或者虚拟机运行进行体验.
docker pull python:2.7
生成子进程
Python 生成子进程使用 os.fork() , 它将产生一个子进程. fork 调用同时在父进程和主进程同时返回, 在父进程中返回子进程的 pid, 在子进程中返回 0, 如果返回值小于零, 说明子进程产生失败, 一般是因为操作系统资源不足.
- import os
- def create_child():
- pid = os.fork()
- if pid> 0:
- print 'in father process'
- return True
- elif pid == 0:
- print 'in child process'
- return False
- else:
- raise
生成多个子进程
我们调用 create_child 方法多次就可以生成多个子进程, 前提是必须保证 create_child 是在父进程里执行, 如果是子进程, 就不要在调用了.
- # coding: utf-8
- # child.py
- import os
- def create_child(i):
- pid = os.fork()
- if pid> 0:
- print 'in father process'
- return pid
- elif pid == 0:
- print 'in child process', i
- return 0
- else:
- raise
- for i in range(10): # 循环 10 次, 创建 10 个子进程
- pid = create_child(i)
- # pid==0 是子进程, 应该立即退出循环, 否则子进程也会继续生成子进程
- # 子子孙孙, 那就生成太多进程了
- if pid == 0:
- break
运行 python child.py , 输出
- in father process
- in father process
- in child process 0
- in child process 1
- in father process
- in child process 2
- in father process
- in father process
- in child process 3
- in father process
- in child process 4
- in child process 5
- in father process
- in father process
- in child process 6
- in child process 7
- in father process
- in child process 8
- in father process
- in child process 9
进程休眠
使用 time.sleep 可以使进程休眠任意时间, 单位为秒, 可以是小数
- import time
- for i in range(5):
- print 'hello'
- time.sleep(1) # 睡 1s
杀死子进程
使用 os.kill(pid, sig_num) 可以向进程号为 pid 的子进程发送信号, sig_num 常用的有 SIGKILL(暴力杀死, 相当于 kill -9),SIGTERM(通知对方退出, 相当于 kill 不带参数),SIGINT(相当于键盘的 ctrl+c).
- # coding: utf-8
- # kill.py
- import os
- import time
- import signal
- def create_child():
- pid = os.fork()
- if pid> 0:
- return pid
- elif pid == 0:
- return 0
- else:
- raise
- pid = create_child()
- if pid == 0:
- while True: # 子进程死循环打印字符串
- print 'in child process'
- time.sleep(1)
- else:
- print 'in father process'
- time.sleep(5) # 父进程休眠 5s 再杀死子进程
- os.kill(pid, signal.SIGKILL)
- time.sleep(5) # 父进程继续休眠 5s 观察子进程是否还有输出
运行 python kill.py , 我们看到控制台输出如下
- in father process
- in child process
- # 等 1s
- in child process
- # 等 1s
- in child process
- # 等 1s
- in child process
- # 等 1s
- in child process
- # 等了 5s
说明 os.kill 执行之后, 子进程已经停止输出了
僵尸子进程
在上面的例子中, os.kill 执行完之后, 我们通过 ps -ef|grep python 快速观察进程的状态, 可以发现子进程有一个奇怪的显示 <defunct>
root 12 1 0 11:22 pts/0 00:00:00 python kill.py
root 13 12 0 11:22 pts/0 00:00:00 [python] <defunct>
待父进程终止后, 子进程也一块消失了. 那 <defunct> 是什么含义呢?
它的含义是僵尸进程. 子进程结束后, 会立即成为僵尸进程, 僵尸进程占用的操作系统资源并不会立即释放, 它就像一具尸体啥事也不干, 但是还是持续占据着操作系统的资源 (内存等).
收割子进程
如果彻底干掉僵尸进程? 父进程需要调用 waitpid(pid, options) 函数,收割子进程, 这样子进程才可以灰飞烟灭. waitpid 函数会返回子进程的退出状态, 它就像子进程留下的临终遗言, 必须等父进程听到后才能彻底瞑目.
- # coding: utf-8
- import os
- import time
- import signal
- def create_child():
- pid = os.fork()
- if pid> 0:
- return pid
- elif pid == 0:
- return 0
- else:
- raise
- pid = create_child()
- if pid == 0:
- while True: # 子进程死循环打印字符串
- print 'in child process'
- time.sleep(1)
- else:
- print 'in father process'
- time.sleep(5) # 父进程休眠 5s 再杀死子进程
- os.kill(pid, signal.SIGTERM)
- ret = os.waitpid(pid, 0) # 收割子进程
- print ret # 看看到底返回了什么
- time.sleep(5) # 父进程继续休眠 5s 观察子进程是否还存在
运行 python kill.py 输出如下
- in father process
- in child process
- in child process
- in child process
- in child process
- in child process
- in child process
- (125, 9)
我们看到 waitpid 返回了一个 tuple, 第一个是子进程的 pid, 第二个 9 是什么含义呢, 它在不同的操作系统上含义不尽相同, 不过在 Unix 上, 它通常的 value 是一个 16 位的整数值, 前 8 位表示进程的退出状态, 后 8 位表示导致进程退出的信号的整数值. 所以本例中退出状态位 0, 信号编号位 9, 还记得 kill -9 这个命令么, 就是这个 9 表示暴力杀死进程.
如果我们将 os.kill 换一个信号才看结果, 比如换成 os.kill(pid, signal.SIGTERM), 可以看到返回结果变成了 (138, 15) ,15 就是 SIGTERM 信号的整数值.
waitpid(pid, 0) 还可以起到等待子进程结束的功能, 如果子进程不结束, 那么该调用会一直卡住.
捕获信号
SIGTERM 信号默认处理动作就是退出进程, 其实我们还可以设置 SIGTERM 信号的处理函数, 使得它不退出.
- # coding: utf-8
- import os
- import time
- import signal
- def create_child():
- pid = os.fork()
- if pid> 0:
- return pid
- elif pid == 0:
- return 0
- else:
- raise
- pid = create_child()
- if pid == 0:
- signal.signal(signal.SIGTERM, signal.SIG_IGN)
- while True: # 子进程死循环打印字符串
- print 'in child process'
- time.sleep(1)
- else:
- print 'in father process'
- time.sleep(5) # 父进程休眠 5s 再杀死子进程
- os.kill(pid, signal.SIGTERM) # 发一个 SIGTERM 信号
- time.sleep(5) # 父进程继续休眠 5s 观察子进程是否还存在
- os.kill(pid, signal.SIGKILL) # 发一个 SIGKILL 信号
- time.sleep(5) # 父进程继续休眠 5s 观察子进程是否还存在
我们在子进程里设置了信号处理函数, SIG_IGN 表示忽略信号. 我们发现第一次调用 os.kill 之后, 子进程会继续输出. 说明子进程没有被杀死. 第二次 os.kill 之后, 子进程终于停止了输出.
接下来我们换一个自定义信号处理函数, 子进程收到 SIGTERM 之后, 打印一句话再退出.
- # coding: utf-8
- import os
- import sys
- import time
- import signal
- def create_child():
- pid = os.fork()
- if pid> 0:
- return pid
- elif pid == 0:
- return 0
- else:
- raise
- def i_will_die(sig_num, frame): # 自定义信号处理函数
- print "child will die"
- sys.exit(0)
- pid = create_child()
- if pid == 0:
- signal.signal(signal.SIGTERM, i_will_die)
- while True: # 子进程死循环打印字符串
- print 'in child process'
- time.sleep(1)
- else:
- print 'in father process'
- time.sleep(5) # 父进程休眠 5s 再杀死子进程
- os.kill(pid, signal.SIGTERM)
- time.sleep(5) # 父进程继续休眠 5s 观察子进程是否还存在
输出如下
- in father process
- in child process
- in child process
- in child process
- in child process
- in child process
- child will die
信号处理函数有两个参数, 第一个 sig_num 表示被捕获信号的整数值, 第二个 frame 不太好理解, 一般也很少用. 它表示被信号打断时, Python 的运行的栈帧对象信息. 读者可以不必深度理解.
多进程并行计算实例
下面我们使用多进程进行一个计算圆周率 PI. 对于圆周率 PI 有一个数学极限公式, 我们将使用该公司来计算圆周率 PI.
先使用单进程版本
- import math
- def pi(n):
- s = 0.0
- for i in range(n):
- s += 1.0/(2*i+1)/(2*i+1)
- return math.sqrt(8 * s)
- print pi(10000000)
输出
3.14159262176
这个程序跑了有一小会才出结果, 不过这个值已经非常接近圆周率了.
接下来我们用多进程版本, 我们用 redis 进行进程间通信.
- # coding: utf-8
- import os
- import sys
- import math
- import redis
- def slice(mink, maxk):
- s = 0.0
- for k in range(mink, maxk):
- s += 1.0/(2*k+1)/(2*k+1)
- return s
- def pi(n):
- pids = []
- unit = n / 10
- client = redis.StrictRedis()
- client.delete("result") # 保证结果集是干净的
- del client # 关闭连接
- for i in range(10): # 分 10 个子进程
- mink = unit * i
- maxk = mink + unit
- pid = os.fork()
- if pid> 0:
- pids.append(pid)
- else:
- s = slice(mink, maxk) # 子进程开始计算
- client = redis.StrictRedis()
- client.rpush("result", str(s)) # 传递子进程结果
- sys.exit(0) # 子进程结束
- for pid in pids:
- os.waitpid(pid, 0) # 等待子进程结束
- sum = 0
- client = redis.StrictRedis()
- for s in client.lrange("result", 0, -1):
- sum += float(s) # 收集子进程计算结果
- return math.sqrt(sum * 8)
- print pi(10000000)
我们将级数之和的计算拆分成 10 个子进程计算, 每个子进程负责 1/10 的计算量, 并将计算的中间结果扔到 redis 的队列中, 然后父进程等待所有子进程结束, 再将队列中的数据全部汇总起来计算最终结果.
输出如下
3.14159262176
这个结果和单进程结果一致, 但是花费的时间要缩短了不少.
这里我们之所以使用 redis 作为进程间通信方式, 是因为进程间通信是一个比较复杂的技术, 我们需要单独一篇文章来仔细讲, 各位读者请耐心听我下回分解, 我们将会使用进程间通信技术来替换掉这里的 redis.
来源: http://www.tuicool.com/articles/3A7fI3Y