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(),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,子进程退出状态:%s' % (
        os.getpid(),child_pid,child_status))

    print('父进程sleep 600 秒,此时用 ps 命令查看进程状态')
    time.sleep(600)

该代码的执行结果如下:

$ python3 Test.py 
这里是子进程,父进程pid:1585,子进程pid:1586 sleep 5 秒
这里是父进程,子进程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' % (
        os.getppid(),os.getpid()
        ))

else:
    # 父进程先 sleep,保证子进程先退出,然后再调用 wait
    time.sleep(5)

    child_pid,此时用 ps 命令查看进程状态')
    time.sleep(600)

该代码执行结果如下:

$ python3 Test.py 
这里是子进程,父进程pid:1591,子进程pid:1592
这里是父进程,子进程pid:1592,此时用 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,接收到了信号:%s,此时用 ps 命令查看进程状态。父进程pid:%s,子进程退出状态:%s' % (
        signum,os.getpid(),child_status))

# 父进程注册信号处理函数
signal.signal(signal.SIGCHLD,sig_handelr)

# 创建子进程
pid = os.fork()

if pid == 0:
    # 子进程范围

    print('这里是子进程,子进程 sleep 10 秒' % (
        os.getppid(),os.getpid()
        ))

    # 先让子进程sleep 10 秒,然后退出
    time.sleep(10)

else:
    print('这里是父进程,父进程sleep 600 秒,保证子进程先退出')
    time.sleep(600)

注意:信号处理函数signal 的调用,一定要在fork 函数之前。

执行结果如下:

$ python3 Test.py 
这里是父进程,保证子进程先退出
这里是子进程,父进程pid:1651,子进程pid:1652,子进程 sleep 10 秒
这里是父进程,接收到了信号:17,此时用 ps 命令查看进程状态。父进程pid:1651,子进程退出状态: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('这里是子进程,保证子进程先退出')
    time.sleep(600)

我们将signal 函数的第二个参数设置为signal.SIG_IGN,意思是忽略掉信号。

执行结果如下:

$ python3 Test.py 
这里是父进程,父进程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 目录操作

欢迎关注作者公众号,获取更多技术干货。

码农充电站pro

相关文章

本文从多个角度分析了vi编辑器保存退出命令。我们介绍了保存...
Python中的回车和换行是计算机中文本处理中的两个重要概念,...
SQL Server启动不了错误1067是一种比较常见的故障,主要原因...
信息模块是一种可重复使用的、可编程的、可扩展的、可维护的...
本文从电脑配置、PyCharm版本、Java版本、配置文件以及程序冲...
本文主要从多个角度分析了安装SQL Server 2012时可能出现的错...