V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
shellquery
V2EX  ›  程序员

深入 Python 多进程编辑基础——图文版

  •  
  •   shellquery · 2018-05-28 14:06:36 +08:00 · 1130 次点击
    这是一个创建于 2152 天前的主题,其中的信息可能已经有所发展或是发生改变。

    多进程编程知识是 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。

    阅读 python 相关高级文章,请关注公众号「码洞」

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1572 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 16:59 · PVG 00:59 · LAX 09:59 · JFK 12:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.