Python 简明教程 --- 26,Python 多进程编程
微信公众号:码农充电站pro
个人主页:https://codeshellme.github.io
学编程最有效的方法是动手敲代码。
目录
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 简明教程 --- 21,Python 继承与多态
Python 简明教程 --- 22,Python 闭包与装饰器
Python 简明教程 --- 23,Python 异常处理
Python 简明教程 --- 24,Python 文件读写
Python 简明教程 --- 25,Python 目录操作
欢迎关注作者公众号,获取更多技术干货。
Python 简明教程 --- 26,Python 多进程编程的更多相关文章
- python中global的用法——再读python简明教程
今天看了知乎@萧井陌的编程入门指南,想重温一下 <python简明教程>,对global的用法一直不太熟练,在此熟练一下,并实践一下python中list.tuple.set作为参数的区别 ...
- python简明教程
Python简明教程 MachinePlay关注 0.7072018.09.26 01:49:43字数 2,805阅读 9,287 Python一小时快速入门 1.Python简介 pylogo. ...
- Python 简明教程 --- 3,Python 基础概念
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 控制复杂性是计算机编程的本质. -- Brian Kernighan 了解了如何编写第一个Pytho ...
- Python 简明教程 --- 18,Python 面向对象
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 代码能借用就借用. -- Tom Duff 目录 编程可分为面向过程编程和面向对象编程,它们是两种不 ...
- Python 简明教程 --- 17,Python 模块与包
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 正确的判断来源于经验,然而经验来源于错误的判断. -- Fred Brooks 目录 我们已经知道函 ...
- Python 简明教程 --- 8,Python 字符串函数
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 好代码本身就是最好的文档.当你需要添加一个注释时,你应该考虑如何修改代码才能不需要注释. -- St ...
- Python 简明教程 ---10,Python 列表
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 程序 = 算法 + 数据结构 -- Nicklaus Wirth 目录 从这句话程序 = 算法 + ...
- Python 简明教程 --- 19,Python 类与对象
微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 那些能用计算机迅速解决的问题,就别用手做了. -- Tom Duff 目录 上一节 我们介绍了Pyt ...
- 《Python简明教程》总结
Python经典教程<Python简明教程> 目录: 为什么Python 安装Python 体验Python Python数据类型 运算符与表达式 控制流 函数 模块 数据结构 解决问题 ...
随机推荐
- Java中List集合去除重复数据的方法1
1. 循环list中的所有元素然后删除重复 public static List removeDuplicate(List list) { for ( int i = ...
- C++多种方法枚举串口号
部分方式没结果,思路应该是没错. //7. std::cout << "M8: SetupDiGetClassDevs " << std::endl; // ...
- scrapy框架结构与工作原理
组件: ENGINE:引擎,框架的核心,其他组件在其控制下协同工作. SCHEDULER:调度器,负责对SPIDER提交的下载请求进行调度 DOWNLOADER:下载器,负责下载页面,发送HTTP请求 ...
- Nginx 介绍和安装(centos7)
本文是作者原创,版权归作者所有.若要转载,请注明出处 什么是 nginx ? Nginx 是高性能的 HTTP 和反向代理的服务器,处理高并发能力是十分强大的,能经受高负 载的考验,有报告表明能支持高 ...
- STA树的深度(树型DP)
STA树的深度 题目大意 给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大 Input 给出一个数字N,代表有N个点.N<=1000000 下面N-1条边. Outpu ...
- CVE-2020-5902 F5 BIG-IP 远程代码执行RCE
cve-2020-5902 : RCE:curl -v -k 'https://[F5 Host]/tmui/login.jsp/..;/tmui/locallb/workspace/tmshCmd. ...
- redis 集群方案及搭建
由于Redis出众的性能,其在众多的移动互联网企业中得到广泛的应用.Redis在3.0版本前只支持单实例模式,虽然现在的服务器内存可以到100GB.200GB的规模,但是单实例模式限制了Redis没法 ...
- Quartz.Net 任务调度
基于ASP.NET MVC(C#)和Quartz.Net组件实现的定时执行任务调度 在之前的文章<推荐一个简单.轻量.功能非常强大的C#/ASP.NET定时任务执行管理器组件–FluentSch ...
- es6 模块与commonJS的区别
在刚接触模块化开发的阶段,我总是容易将export.import.require等语法给弄混,今天索性记个笔记,将ES6 模块知识点理清楚 未接触ES6 模块时,模块开发方案常见的有CommonJS. ...
- 手写一个React-Redux,玩转React的Context API
上一篇文章我们手写了一个Redux,但是单纯的Redux只是一个状态机,是没有UI呈现的,所以一般我们使用的时候都会配合一个UI库,比如在React中使用Redux就会用到React-Redux这个库 ...