Python 简明教程 --- 26,Python 多进程编程
目录
微信公众号:码农充电站pro
学编程最有效的方法是动手敲代码。
目录
1,什么是多进程
我们所写的Python 代码就是一个程序
,Python 程序用Python 解释器来执行。程序是存储在磁盘上的一个文件,Python 程序需要通过Python 解释器将其读入内存,然后进行解释
并执行
。
处于执行
(运行
)状态的程序叫做进程
。进程是由操作系统
分配资源并进行调度才能执行。操作系统会为每个进程分配进程ID
(非负整数),作为进程的唯一标识
。
现代操作系统都提供了多进程
同步执行的机制,也就是操作系统允许多个进程同时运行。操作系统负责进程的管理工作。比如我们在处理word
文档的同时还在听音乐,这就需要有一个word
程序和一个音乐软件在同步运行。
多进程机制的硬件支持是由CPU
提供的,CPU 有单核
与多核
之分。
单核CPU
只有一个核心,在同一时刻只能有一个进程在执行,单核CPU 上的多个进程的执行,实际上是并发
执行。其背后的原理是,CPU 的运行速度是相当快的,多进程执行实际上是每个进程间隔运行
,而间隔的时间非常短,人类是无法察觉到这种间隔的,这样,人类感觉起来就像多个进程同时执行一样。
多核CPU
有多个核心,每个核心都可以处理进程,这样每个进程都可以运行在不同的CPU 上,这叫做并行
执行,是真正的在同一时刻运行。
2,fork
函数
Python 语言也支持多进程编程,以此来支持更加复杂的,高性能的应用。
为了支持多进程编程,操作系统提供了最原始的系统调用fork()
函数,使得当前进程可以创建出一个子进程,这样父进程和子进程就可以处理不同的事务。
Python 中的fork()
函数被封装在os
模块中,该函数原型很简单,没有任何参数,如下:
fork()
与一般函数不同的是,该函数的返回值比较特殊,fork
函数执行一次,返回两次值:
- 返回值为0: 为子进程范围,子进程可通过
getppid()
函数得到父进程ID - 返回值为子进程ID: 为父进程范围,这样父进程可得到子进程ID
示例:
#! /usr/bin/env python3
import os
# 这里是父进程
# 创建子进程
pid = os.fork()
if pid == 0:
# 子进程范围,编写子进程需要处理的事务
print('这里是子进程,父进程ID 为:%s,子进程ID 为:%s' % (
os.getppid(), os.getpid()))
else:
# 父进程范围,编写父进程需要处理的事务
print('这里是父进程, 父进程ID 为:%s, 子进程ID 为:%s' % (
os.getpid(), pid))
# 父进程和子进程都会执行到这里
print('进程ID:%s' % os.getpid())
在上面代码中,我们调用了fork()
函数,返回值为pid
:
- pid 为0 时: 进入了子进程范围,我们使用
getppid()
函数获取了父进程ID,使用getpid()
函数获取了当前进程(子进程)ID - pid 不为0 时: 进入了父进程范围,此时
pid
就是子进程ID,我们使用getpid()
函数获取了当前进程(父进程)ID
代码的最后一行print('进程ID:%s' % os.getpid())
,父进程和子进程都会执行到。
这段代码的执行结果如下:
$ python3 Test.py
这里是父进程, 父进程ID 为:1405, 子进程ID 为:1406
进程ID:1405 # 最后一行代码的输出
这里是子进程,父进程ID 为:1405,子进程ID 为:1406
进程ID:1406 # 最后一行代码的输出
从上面的执行结果,我们可以看到,父进程ID 为 1405
,子进程ID 为1406
。
最后一行代码,子进程和父进程都能执行到的原因是,在执行了fork()
函数后,之后的代码就同时存在于两个进程(父子进程)空间中。返回值pid
为0
时,是子进程空间;返回值pid
不为0
时,是父进程空间。
而最后一行代码,即属于pid == 0
的范围,又属于else
的范围,所以父子进程都会执行该代码。
3,孤儿进程与僵尸进程
我们已经知道,在fork()
函数之后,就会有两个进程,分别是父进程
和子进程
。那这两个进程是哪个先执行呢?是父进程先于子进程执行,还是子进程先于父进程执行?
答案是不确定
。因为父子进程哪个先执行不是程序能够决定的,而是由操作系统的调度决定的,操作系统先调度到谁,谁就先执行。
另外,在父子进程退出时,由于退出的先后顺序不一样,也会造成孤儿进程
与僵尸进程
:
- 孤儿进程:父进程先于子进程退出,子进程会变成孤儿进程。孤儿进程会被
系统进程
接管,系统进程变成孤儿进程的父进程。在孤儿进程退出时,系统进程会进行处理。 - 僵尸进程:如果子进程退出时,其父进程没有处理子进程的
退出状态
,那么这个进程退出后,其占用的系统资源就不会释放
,也就是,这个进程即不进行正常的工作,却依然占用系统资源,这样的进程叫做僵尸进程
。
下面我们编写一段会产生僵尸进程的代码:
#! /usr/bin/env python3
import os
import time
# 这里是父进程
# 创建子进程
pid = os.fork()
if pid == 0:
# 子进程范围,编写子进程需要处理的事务
print('这里是子进程,父进程ID 为:%s,子进程ID 为:%s' % (
os.getppid(), os.getpid()))
else:
# 父进程范围,编写父进程需要处理的事务
print('这里是父进程, 父进程ID 为:%s, 子进程ID 为:%s' % (
os.getpid(), pid))
print('父进程正在sleep 600S...')
time.sleep(600)
# 父进程和子进程都会执行到这里
print('进程ID:%s' % os.getpid())
上面的代码中,我们在父进程中sleep
了600
秒,这样,子进程会先于父进程退出,而父进程没有处理子进程的退出状态,这必然造成子进程变为僵尸进程。
我们使用python3
执行该程序,如下:
$ python3 Test.py
这里是父进程, 父进程ID 为:1524, 子进程ID 为:1525
父进程正在sleep 600S...
这里是子进程,父进程ID 为:1524,子进程ID 为:1525
进程ID:1525
`注意,这里父进程在sleep,程序并没有退出`
从上面的输出,我们可以知道,父进程ID 为 1524
,子进程ID 为1525
。
然后,我们用ps
命令,来查看当前的python3
进程,如下:
$ ps -aux| grep python3
1 2 3 4 5 6 7 8 9 10 11
wp 1524 1.0 0.0 23992 6604 pts/2 `S` 09:13 0:00 python3 Test.py
wp 1525 0.0 0.0 0 0 pts/2 `Z` 09:13 0:00 [python3] <defunct>
(为了方便查看,我在上面的输出中添加了列数
,共11 列。)
其中第 2 列为进程ID,第 8 列为进程状态。我们看到父进程(1524)处于S
状态(即休眠状态),子进程(1525)处于Z
状态(即僵尸状态)。
这说明,子进程先于父进程退出,而父进程又没有处理子进程的退出状态,所以使得子进程变为了僵尸进程
。
4,避免僵尸进程
孤儿进程不会造成什么危害,而僵尸进程会造成系统资源浪费,所以僵尸进程是应该被避免的情况。
既然僵尸进程会导致资源浪费的情况,那么操作系统为什么还要设计僵尸进程的存在呢?
僵尸进程存在的意义是保存了进程退出时的一些状态,比如进程ID,终止状态,资源使用情况等信息,这些信息都可以让其父进程获取到,来做适当的处理。
所以,在子进程退出后,只有经过父进程的处理才能避免僵尸进程
的出现。
wait
函数
父进程可以通过wait()
函数来获取子进程的退出状态。需要说明的是,调用wait()
函数的进程将会阻塞,直到该进程的某个子进程退出。
wait
函数原型如下:
wait()
`
该函数返回一个元组(pid, status)
pid 为退出进程的ID
status 为退出进程的状态
`
父进程调用wait()
函数有两种情况,这两种情况都会正确的避免僵尸进程
的出现:
- 父进程在子进程退出
前
调用wait()
- 父进程在子进程退出
后
调用wait()
我们分别对这两种情况进行代码演示,通过sleep
函数来控制哪个进程先退出:
- 父进程在子进程退出
前
调用wait()
代码:
#! /usr/bin/env python3
import os
import time
# 这里是父进程
# 创建子进程
pid = os.fork()
if pid == 0:
# 子进程调用sleep,保证父进程先调用wait
print('这里是子进程, 父进程pid:%s, 子进程pid:%s sleep 5 秒' % (
os.getppid(), os.getpid()
))
time.sleep(5)
else:
# 父进程调用wait,且出阻塞在这里
child_pid, child_status = os.wait()
print('这里是父进程, 父进程pid:%s, 子进程pid:%s, 子进程退出状态:%s' % (
os.getpid(), child_pid, child_status))
print('父进程sleep 600 秒, 此时用 ps 命令查看进程状态')
time.sleep(600)
该代码的执行结果如下:
$ python3 Test.py
这里是子进程, 父进程pid:1585, 子进程pid:1586 sleep 5 秒
这里是父进程, 父进程pid:1585, 子进程pid:1586, 子进程退出状态:0
父进程sleep 600 秒, 此时用 ps 命令查看进程状态
当打印出父进程sleep 600 秒, 此时用 ps 命令查看进程状态
这句话时,证明子进程
已经退出,我们用ps
命令查看python3
进程状态,如下:
$ ps -aux| grep python3
1 2 3 4 5 6 7 8 9 10 11
wp 1585 0.0 0.0 23992 6604 pts/2 S 10:10 0:00 python3 Test.py
可见此时只有父进程存活,子进程已经成功退出,没有处于僵尸进程状态。
- 父进程在子进程退出
后
调用wait()
代码:
#! /usr/bin/env python3
import os
import time
# 这里是父进程
# 创建子进程
pid = os.fork()
if pid == 0:
# 子进程范围
print('这里是子进程, 父进程pid:%s, 子进程pid:%s' % (
os.getppid(), os.getpid()
))
else:
# 父进程先 sleep,保证子进程先退出,然后再调用 wait
time.sleep(5)
child_pid, child_status = os.wait()
print('这里是父进程, 父进程pid:%s, 子进程pid:%s, 子进程退出状态:%s' % (
os.getpid(), child_pid, child_status))
print('父进程sleep 600 秒, 此时用 ps 命令查看进程状态')
time.sleep(600)
该代码执行结果如下:
$ python3 Test.py
这里是子进程, 父进程pid:1591, 子进程pid:1592
这里是父进程, 父进程pid:1591, 子进程pid:1592, 子进程退出状态:0
父进程sleep 600 秒, 此时用 ps 命令查看进程状态
当打印出父进程sleep 600 秒, 此时用 ps 命令查看进程状态
这句话时,我们用ps
命令查看python3
进程状态,如下:
执行结果:
$ ps -aux| grep python3
1 2 3 4 5 6 7 8 9 10 11
wp 1591 0.2 0.0 23992 6620 pts/2 S 10:20 0:00 python3 Test.py
可见此时只有父进程存活,子进程已经成功退出,没有处于僵尸进程状态。
5,使用信号处理僵尸进程
因为wait()
函数会导致调用进程阻塞,那就使得调用进程无法处理别的事情。这其实不是很合理,因为白白浪费了一个进程。
这种情况我们可以使用信号
来处理。
信号
是一种系统中断,当进程遇到系统中断时,就会打断进程正在执行的正常流程,转而去处理中断函数
。进程处理完中断函数后,又会回到进程原来的处理流程。
中断函数
是用户向系统注册的一个函数,用于在遇到某个信号时,要做哪些处理。
因为子进程在退出时会向父进程发送SIGCHLD
信号,所以父进程可以通过捕获该信号来处理子进程。
signal
模块
在Linux 系统中,我们可以通过kill -l
命令来查看系统中的信号,共64
个信号:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
在Python 中通过signal
模块来处理信号,我们通过dir(signal)
来查看signal
模块都有哪些内容:
>>> dir(signal)
['Handlers', 'ITIMER_PROF', 'ITIMER_REAL',
'ITIMER_VIRTUAL', 'ItimerError', 'NSIG',
'SIGABRT', 'SIGALRM', 'SIGBUS', 'SIGCHLD',
'SIGCLD', 'SIGCONT', 'SIGFPE', 'SIGHUP',
'SIGILL', 'SIGINT', 'SIGIO', 'SIGIOT',
'SIGKILL', 'SIGPIPE', 'SIGPOLL', 'SIGPROF',
'SIGPWR', 'SIGQUIT', 'SIGRTMAX', 'SIGRTMIN',
'SIGSEGV', 'SIGSTOP', 'SIGSYS', 'SIGTERM',
'SIGTRAP', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU',
'SIGURG', 'SIGUSR1', 'SIGUSR2', 'SIGVTALRM',
'SIGWINCH', 'SIGXCPU', 'SIGXFSZ', 'SIG_BLOCK',
'SIG_DFL', 'SIG_IGN', 'SIG_SETMASK',
'SIG_UNBLOCK', 'Sigmasks', 'Signals',
'_IntEnum', '__builtins__', '__cached__',
'__doc__', '__file__', '__loader__',
'__name__', '__package__', '__spec__',
'_enum_to_int', '_int_to_enum', '_signal',
'alarm', 'default_int_handler', 'getitimer',
'getsignal', 'pause', 'pthread_kill',
'pthread_sigmask', 'set_wakeup_fd', 'setitimer',
'siginterrupt', 'signal', 'sigpending',
'sigtimedwait', 'sigwait', 'sigwaitinfo',
'struct_siginfo']
可以看到,signal
模块中包含了一些信号相关函数,和绝大部分信号。
signal
函数
要想处理信号,则需要使用signal
模块中的signal
函数向系统注册,捕获哪个信号,以及处理该信号的函数。
signal
函数原型如下:
signal(signalnum, handler)
- 该函数接收两个参数,分别是
signalnum
和handler
signalnum
是要捕获的信号handler
是信号处理函数
handler
参数有三种取值:
SIG_DFL
:表示系统设置的默认值SIG_IGN
:表示忽略该信号- 一个函数类型的参数:该函数接收两个参数分别是
信号编号
和当前的栈帧
接下来,我们编写代码,用信号来处理僵尸进程。
示例代码:
#! /usr/bin/env python3
import os
import time
import signal
# 这里是父进程
# 信号处理函数
# 该函数须有两个参数
def sig_handelr(signum, frame):
# print(frame)
# 父进程中调用 wait 来处理子进程
child_pid, child_status = os.wait()
print('这里是父进程, 接收到了信号:%s, 此时用 ps 命令查看进程状态。父进程pid:%s, 子进程pid:%s, 子进程退出状态:%s' % (
signum, os.getpid(), child_pid, child_status))
# 父进程注册信号处理函数
signal.signal(signal.SIGCHLD, sig_handelr)
# 创建子进程
pid = os.fork()
if pid == 0:
# 子进程范围
print('这里是子进程, 父进程pid:%s, 子进程pid:%s, 子进程 sleep 10 秒' % (
os.getppid(), os.getpid()
))
# 先让子进程sleep 10 秒,然后退出
time.sleep(10)
else:
print('这里是父进程, 父进程sleep 600 秒, 保证子进程先退出')
time.sleep(600)
注意:信号处理函数signal
的调用,一定要在fork
函数之前。
执行结果如下:
$ python3 Test.py
这里是父进程, 父进程sleep 600 秒, 保证子进程先退出
这里是子进程, 父进程pid:1651, 子进程pid:1652, 子进程 sleep 10 秒
这里是父进程, 接收到了信号:17, 此时用 ps 命令查看进程状态。父进程pid:1651, 子进程pid:1652, 子进程退出状态:0
`这里程序并没有退出,因为父进程在sleep 600 秒`
等待子进程sleep
10 秒,退出之后,我们用ps
命令查看进程状态:
ps -aux| grep python3
1 2 3 4 5 6 7 8 9 10 11
wp 1651 0.0 0.0 23992 6708 pts/2 S 21:38 0:00 python3 Test.py
通过ps
命令可以看出,在子进程退出之后,并没有变成僵尸进程,说明我们的处理没有问题。
6,忽略SIGCHLD
信号
更简单处理办法是直接将SIGCHLD
信号忽略
掉,而不需要为信号注册处理函数
。忽略信号
也是处理信号的一种,同样不会使子进程变成僵尸进程。
代码如下:
#! /usr/bin/env python3
import os
import time
import signal
# 这里是父进程
# 父进程注册信号,处理方法是忽略
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
# 创建子进程
pid = os.fork()
if pid == 0:
# 子进程范围
print('这里是子进程, 父进程pid:%s, 子进程pid:%s, 子进程 sleep 10 秒' % (
os.getppid(), os.getpid()
))
# 先让子进程sleep 10 秒,然后退出
time.sleep(10)
else:
print('这里是父进程, 父进程sleep 600 秒, 保证子进程先退出')
time.sleep(600)
我们将signal
函数的第二个参数设置为signal.SIG_IGN
,意思是忽略
掉信号。
执行结果如下:
$ python3 Test.py
这里是父进程, 父进程sleep 600 秒, 保证子进程先退出
这里是子进程, 父进程pid:1659, 子进程pid:1660, 子进程 sleep 10 秒
`这里程序并没有退出,因为父进程在sleep 600 秒`
我们再用 ps
命令输出如下:
$ ps -aux| grep python3
1 2 3 4 5 6 7 8 9 10 11
wp 1659 0.1 0.0 23992 6688 pts/2 S 21:57 0:00 python3 Test.py
可以看到,子进程依然没有变成僵尸进程。
(完。)
推荐阅读:
Python 简明教程 — 22,Python 闭包与装饰器
欢迎关注作者公众号,获取更多技术干货。
文章作者 @码农加油站
上次更改 2020-07-23