Python 简明教程 — 26,Python 多进程编程

微信扫一扫,分享到朋友圈

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() 函数后,之后的代码就同时存在于两个进程(父子进程)空间中。返回值 pid0 时,是子进程空间;返回值 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())

上面的代码中,我们在父进程中 sleep600 秒,这样,子进程会先于父进程退出,而父进程没有处理子进程的退出状态,这必然造成子进程变为僵尸进程。

我们使用 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 函数来控制哪个进程先退出:

  1. 父进程在子进程退出 调用 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

可见此时只有父进程存活,子进程已经成功退出,没有处于僵尸进程状态。

  1. 父进程在子进程退出 调用 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)
  • 该函数接收两个参数,分别是 signalnumhandler
  • 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 简明教程 — 26,Python 多进程编程

成立10年持续亏损 新冠疫苗第一股康希诺即将回A股

上一篇

玩梗半年 互联网“公敌”肖战为何热度不减?

下一篇

你也可能喜欢

Python 简明教程 — 26,Python 多进程编程

长按储存图像,分享给朋友