Python多任务编程之进程

多任务编程:

  1. 作用:充分利用计算机多核资源,提高程序的运行效率。
  2. 实现方案:多进程;多线程
  3. 相关概念:
    1. 并发:多个任务在同一时间间隔内发生。表面上看像是多个任务同时进行,实际是任务在时间片上的轮转(即多个任务在内核上以极短的时间快速切换),也就是说,每个时刻只有一个任务占有内核资源。
    2. 并行:多个任务利用计算机多核资源而同时执行,称这些多个任务间为并行关系

一、进程概述

1、定义

进程(process)是计算机中已运行的程序的实体。
【注】程序是可执行的文件,它静态地占有磁盘空间;而进程是动态地过程,占有计算机运行资源,具有生命周期。

2、进程在OS中的产生过程

进程产生过程

3、相关概念

CPU时间片:系统将CPU的执行时间分割为一个个时间段,称为时间片。
PCB(进程控制块):内存中一个专门的数据结构,用于存放进程信息,使参与并发执行的程序能独立运行,也便于系统唯一标识;PCB包含进程描述信息、进程控制和管理信息、资源分配清单 和 处理机相关信息。
PID(进程标识符):系统为每个进程分配的一个大于0的整数,用来唯一地标识进程。
父子进程:系统中每个进程(除系统初始化进程外)都有唯一的父进程,可以有0或多个子进程;父子进程的关系便于进程管理。

4、进程的状态与转换

  • 五态
    1. 就绪态:进程处于准备运行状态,它已获得了除CPU之外的一切资源,等待分配CPU资源。
    2. 运行态:进程在CPU上运行(某一时刻上占有CPU时间片)。
    3. 阻塞态:进程因等待某一事件(包括除CPU以外一切所需的资源)而暂停运行,让出CPU;又称为等待态。
    4. 创建状态:创建一个新的进程,也即,获取除CPU外资源的过程;详细步骤:申请空白PCB,并填充控制和管理进程的信息;系统分配资源。
    5. 结束状态:进程从系统中消失(进程正常结束或意外中断),所占用的资源得到释放。
      进程的状态与转换

5、进程的特点

  1. 进程可以使用计算机的多核资源
  2. 进程是计算机分配资源的最小单位
  3. 进程之间的运行相互独立、互不影响
  4. 每个进程拥有独立的空间,各自使用自己空间内的资源

Python创建进程常用的有os.fork()函数、multiprocessing模块和Pool进程池

二、基于fork函数的多进程

【注】os.fork()函数只适用于Unix/Linux/Mac系统上运行,在Windows操作系统中不可用。

1、os.fork()函数

pid = os.fork()
功能: 创建新的进程
返回值:整数
      负数——进程创建失败返回pid一个负数
      正数——进程创建成功返回pid一个正数,表示新进程的PID
            创建进程成功后,在新进程中返回0

  • 注意:
  1. 子进程复制父进程的全部内存空间,但从调用fork函数的下一句开始执行
  2. 父子进程各自独立运行,运行顺序不一定,父子进程对各自内存空间的操作不会相互影响
import os
from time import sleep

pid = os.fork()

if pid < 0:
    print("Create process failed")    
elif pid == 0:
    sleep(5)
    print("The new process", os.getpid())
else:
    sleep(4)
    print("The old process", os.getpid())

print("Fork test over")

2、os模块中与进程相关的其他函数

os.getpid()
返回值:返回当前进程的PID

os.getppid()
返回值:返回当前进程的父进程的PID

os._exit(n)
功能:以状态码n退出进程
注解:退出的标准方法是使用sys.exit([n]),_exit()通常只用在fork()子进程中

3、孤儿进程与僵尸进程

  1. 孤儿进程:父进程先于子进程退出,此时子进程称为孤儿进程。孤儿进程会被系统进程收养,此时系统进程就会成为孤儿进程新的父进程。若孤儿进程退出,这个新的父进程会自动处理孤儿进程的退出状态。
  2. 僵尸进程:子进程先于父进程退出,而父进程没有处理子进程的退出状态,此时子进程称为僵尸进程。僵尸进程虽然结束,但是会存留部分PCB在内存中,大量的僵尸进程会浪费系统的内存资源。
  3. 如何避免僵尸进程的产生
    1. os模块中使用wait函数处理子进程退出
      1. 在创建二级子进程的情况下,使用wait函数处理僵尸进程
    2. 通过signal处理子进程退出
      1. 原理:子进程退出时会发送信号给父进程,如果父进程忽略子进程信号,那么系统会自动处理子进程的退出
      2. 方法:调用标准库模块signal中的singal函数,如下:

        import signal
        signal.signal(singal.SIGCHLD, singal.SIG_IGN)

      3. 特点:非阻塞函数,不影响父进程进行,可以处理所有子进程退出。

①.案例一:使用wait函数处理子进程退出

pid, status = os.wait()
功能: 在父进程中阻塞,等待处理子进程的退出。
返回值:pid     退出的子进程的PID
              status 子进程的退出状态,一个16位数字

pid, status = os.waitpid(pid, option)
功能:在父进程中处理子进程的退出状态
参数:pid    -1  表示等待任意子进程退出
                   >0  表示等待指定的子进程退出
返回值:pid  退出的子进程的PID
              status  子进程退出状态,一个16位数字

import os
pid = os.fork()

if pid < 0:
    print("Error")
elif pid == 0:
    print("Child process:", os.getpid())
    os._exit(1)
else:
    pid, status = os.wait()  # 等待处理僵尸进程(必须子进程做完才能执行父进程)
    # pid, status = os.waitpid(-1, os.WNOHANG)  # 非阻塞状态
    print("pid:", pid)
    print("status:", os.WEXITSTATUS(status))

②.案例二:在创建二级子进程的情况下处理僵尸进程

"""
(1)父进程创建子进程,等待回收子进程
(2)子进程创建二级子进程然后退出
(3)二级子进程称为孤儿,和原来的父进程一同执行事件
"""
import os
from time import *

def f1():
    for i in range(4):
        sleep(2)
        print("写代码......")

def f2():
    for i in range(5):
        sleep(5)
        print("测代码......")

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

if pid < 0:
    print("Error")
elif pid == 0:
    p = os.fork()             # 子进程创建二级子进程
    if p == 0:
        f2()				  # 二级子进程执行f2函数
    else:
        os._exit(0)           # 子进程创建二级子进程后立即退出
else:
    os.wait()			      # 父进程等待回收子进程
    f1()

③.案例三:通过signal处理子进程退出

"""signal方法避免僵尸进程的产生"""
import os
import time
import signal

signal.signal(signal.SIGCHLD, signal.SIG_IGN) # 子进程发出退出信号后父进程进行忽略
pid = os.fork()

if pid < 0:
    print("Error")
elif pid == 0:
    print("Child PID:", os.getpid())
else:
    while True:
        time.sleep(8)
        break

三、基于multiprocessing.Process的多进程

multiprocessing模块可在Unix/Linux/Mac/Windows系统上运行

1、思路

使用multiprocessing模块的Process类创建进程的思路如下:
Process使用思路

2、实现方法

①.创建进程对象
  • multiprocessing模块提供了一个Process类代表一个进程对象,语法如下:

from multiprocessing import Process
p = Process([group, [target, [name, [args, [kwargs, [daemon]]]]]])
参数说明如下
      group:参数未使用,保留参数,值始终是None
      target:可调用对象,一般绑定要执行的目标函数
      name:为当前进程实例起的别名,默认命名格式Process-N
      args:表示传递给target函数的参数元组
      kwargs:表示传递给target函数的参数字典
      daemon:默认None,表示从创建的进程继承;实际用户自定义进程一般为默认值为False
注意:

  1. 使用multiprocessing模块同样会复制父进程的内存空间,父子进程互不影响
  2. 子进程只运行target绑定的函数部分
  3. 惯常的用法,multiprocessing的主进程只用来创建和回收子进程,其他事件交由子进程处理
  4. multiprocessing创建的子进程无法使用标准输入
②.启动进程

p.start()
功能:启动进程活动,该方法每个进程对象只能用一次;
说明:启动进程从绑定的target函数开始,该函数作为子进程的执行内容,而进程也被真正创建。

③.回收进程

p.join([timeout])
功能:阻塞等待回收进程,或等待多少秒(用来避免僵尸进程的产生)
参数:可选参数timeout表示超时时间;若采用默认值None,则阻塞直到调用join()方法的进程终止。

from multiprocessing import Process
from time import sleep

a = 1

# 子进程执行函数
def fun1():
    print("子进程1开始执行")
    global a
    print("a=", a)
    a = 10000
    sleep(3)
    print("子进程1执行完毕")

def fun2():
    sleep(2)
    print("子进程2")

if __name__ == "__main__":
    funs = [fun1, fun2]
    tasks = []
    # 创建进程对象
    for i in funs:
        p = Process(target=i)
        tasks.append(p)   # 用列表保存进程对象
        p.start()
    # 启动进程(创建原进程的子进程,子进程执行传参的函数)
    sleep(1)
    print("父进程干点事")
    # 回收进程
    for i in tasks:
        i.join()          # 子进程随父进程退出时没有使用join,则会成为僵尸进程
    print("parent a:", a)
④.进程对象的其他相关方法/属性

p.run()           标准 run() 方法将传递给对象构造函数的可调用对象(即target)作为目标参数,若不指定target,那么就会默认执行Process的run方法
p.terminate() 不管任务是否完成,立即终止
p.name          进程的别称
p.pid              进程的PID
p.is_alive       判断进程实例是否还在执行

p.daemon      设置父子进程的退出关系

  1. 初始值继承自创建进程
  2. 若设置为True,那么子进程会随父进程的退出而结束(换种说法,当进程退出时,它会尝试终止其所有作为守护进程的子进程)
  3. 要求必须在start()前设置
  4. daemon=True的效果相当于join(),所以设置daemon=True后不需要使用join()
from multiprocessing import Process
from time import sleep, ctime

def tm(loop):
    for i in range(loop):
        sleep(2)
        print(ctime())

if __name__ == "__main__":
    p = Process(target=tm, name="owhyt", args=(3, ))
    # 子进程随父进程退出
    p.daemon = True

    p.start()
    print("Name:", p.name)  # 获取名称
    print("PID:", p.pid)  # 获取PID
    print("is Alive:", p.is_alive())  # 是否在生命周期
    # p.join()
    # 子进程随父进程退出时没有使用join,则会成为僵尸进程
⑤.自定义进程类——重写Process类的run()方法
  1. 创建步骤
    1. 继承Process类
    2. 重写__init__方法添加自定义属性,super方法加载父类属性
    3. 重写run()方法
  2. 使用方法
    1. 实例化自定义进程类
    2. 调用start()方法,会自动执行run()方法
    3. 调用join()回收进程
from multiprocessing import Process
import time
import os

#继承Process类
class SubProcess(Process):
    # 由于Process类本身也有__init__初识化方法,这个子类相当于重写了父类的这个方法
    def __init__(self,interval,name='', target=None, args=()):
        super(SubProcess, self).__init__() # 调用Process父类的初始化方法
        # Process.__init__(self)     
        self.interval = interval   # 接收参数interval
        if name:                   # 判断传递的参数name是否存在
            self.name = name       # 如果传递参数name,则为子进程创建name属性,否则使用默认属性
    #重写了Process类的run()方法
    def run(self):
        print("子进程(%s) 开始执行,父进程为(%s)"%(os.getpid(),os.getppid()))
        t_start = time.time()
        time.sleep(self.interval)
        t_stop = time.time()
        print("子进程(%s)执行结束,耗时%0.2f秒"%(os.getpid(),t_stop-t_start))

def child_1(interval):
    print("子进程(%s)开始执行,父进程为(%s)" % (os.getpid(), os.getppid()))
    t_start = time.time()   # 计时开始
    time.sleep(interval)    # 程序将会被挂起interval秒
    t_end = time.time()     # 计时结束
    print("子进程(%s)执行时间为'%0.2f'秒"%(os.getpid(),t_end - t_start))

if __name__=="__main__":
    print("------父进程开始执行-------")
    print("父进程PID:%s" % os.getpid())                  # 输出当前程序的ID
    p1 = SubProcess(target=child_1, args=(1, ), interval=1, name='owhyt')
    p2 = SubProcess(interval=2)
    #对一个不包含target属性的Process类执行start()方法,就会运行这个类中的run()方法
    p1.start()  # 启动进程p1
    p2.start()  # 启动进程p2,这里会执行p2.run()
    # 输出p1和p2进程的执行状态,如果真正进行,返回True,否则返回False
    print("p1.is_alive=%s"%p1.is_alive())
    print("p2.is_alive=%s"%p2.is_alive())
    #输出p1和p2进程的别名和PID
    print("p1.name=%s"%p1.name)
    print("p1.pid=%s"%p1.pid)
    print("p2.name=%s"%p2.name)
    print("p2.pid=%s"%p2.pid)
    print("------等待子进程-------")
    p1.join() # 等待p1进程结束
    p2.join() # 等待p2进程结束
    print("------父进程执行结束-------")

四、基于进程池multiprocessing.Pool的多进程

multiprocessing模块的Pool类,即Pool进程池,它解决实例化多个Process类(几十上百甚至更多)带来的相关问题。

1、进程池的必要性

  • 进程的创建和销毁过程消耗的资源较大
    • 尤其在任务众多、任务执行时间短、任务运行密集时,需频繁地创建和销毁进程,容易造成计算机的高负荷

2、进程池的原理

  • 进程池创建一定数量的进程来处理事件,事件处理完后,进程不退出继续处理其他事件,直到所有事件全都处理完毕,再统一销毁进程。这样,可以增加进程的重复利用,降低资源消耗。

3、进程池的使用

from multiprocessing import Pool

①.创建进程池对象,放入一定数量的进程

pool = Pool([processes])
功能:创建进程池对象
参数:指定进程数量,若不指定数量,则系统自动调用os.cpu_count()返回的值

②.将事件加入进程池队列执行

pool.apply_async(func, [args, [kwds]]])
功能:以非阻塞方式执行func封装的事件,多个任务在进程池中并发执行
参数:func是事件函数
           args 元组类型,给func按位置传参
           kwds 字典类型,给func按键值对传参
返回值:AsyncResult对象(可称事件函数对象)

pool.apply(func, [args, [kwds]]]
功能:以阻塞方式执行func封装的事件,func只在进程池中的一个工作进程中执行,必须等待上一个进程退出才能执行下一个进程,换言之,任务是顺序执行。
参数:func是事件函数
注意:该apply方法不建议使用

③.关闭进程池

pool.close()
功能:关闭进程池,使其不再接受新的任务,当所有任务执行完成后,工作进程会退出。

pool.terminate()
功能:立即停止工作进程,不再处理未完成的任务。

④.回收进程池中的进程

pool.join()
功能:主进程阻塞,等待子进程的退出(回收进程池中的进程),必须在close或terminate之后使用

from multiprocessing import Pool
from time import sleep, ctime

# 进程池事件
def worker(msg):
    sleep(2)
    print(msg)
    return ctime()

if __name__ == "__main__":
    # 创建进程池
    # pool = Pool()
    pool = Pool(5)

    # 向进程池中添加事件
    for i in range(20):
        msg = "Hello %d" % i
        r = pool.apply_async(func=worker, args=(msg,))
    # 关闭进程池(进程池中不再接纳新的事件)
    pool.close()

    # 回收进程池(阻塞/等待现有的进程池事件执行完毕)
    pool.join()

    print(r.get())  # 获取最后一个事件函数的返回值
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值