浅谈Python多进程中的multiprocessing

2022-07-31,,,

前言:本博文主要Python多进程中的multiprocessing

友情链接:

  1. 浅谈Python多进程中的fork
  2. 浅谈Python并发编程之进程(守护进程、锁、信号量)

文章目录

    • 一、多任务的引入
    • 二、多任务的概念
    • 三、进程和程序的区别
    • 四、multiprocessing
    • 五、Process语法结构
    • 六、进程的创建-Process子类
    • 七、Process中kill的方法
    • 八、进程的特性
      • 8.1 不共享全局变量
      • 8.2 所有子进程结束主进程才会结束
    • 九、单进程与多线程的优劣
      • 9.1 单进程
      • 9.2 多进程
      • 9.3 优劣对比

一、多任务的引入

在现实生活中,有很多的场景中的事情是同时进行的,比如开车的时候,手和脚共同来驾驶汽车;再比如,唱歌跳舞也是同时进行的。

下来,我们在程序里面,模拟一下“唱歌跳舞”这件事情,如下:

from time import sleep


def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)

        
def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)

        
if __name__ == '__main__':
    sing() #唱歌
    dance() #跳舞

注意:

  1. 很显然刚刚的程序并没有完成唱歌和跳舞同时进行的要求。
  2. 如果想要实现“唱歌跳舞”同时进行,那么就需要一个新的方法,叫做:多任务。

二、多任务的概念

什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,我们一边在用浏览器上网,一边在听MP3,一边在用Word写作业,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

现在,多核CPU已经非常普及了。但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢?

答案就是操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

实现多任务的策略(调度算法):

  1. 时间片轮转
  2. 优先级调度

实现多任务的三种方式:

  1. 进程
  2. 线程
  3. 协程

多任务的原理:

  1. 并发:假的多任务,时间片的轮转,快速的交替运行任务。
  2. 并行:真的多任务,一个核处理一个任务。

三、进程和程序的区别

编写完毕的代码,在没有运行的时候,称之为程序。正在运行着的代码,就成为进程。

进程,除了包含代码以外,还有需要运行的环境等,所以和程序是有区别的。

四、multiprocessing

如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?

由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。

multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:

from multiprocessing import Process
import os


# 子进程要执行的代码
def run_proc(name):
    print('子进程运行中,name= %s ,pid=%d...' % (name, os.getpid()))

    
if __name__ == '__main__':
    print('父进程 %d.' % os.getppid())
    p = Process(target=run_proc, args=('test',))
    print('子进程将要执行')
    p.start()
    p.join()
    print('子进程已结束')

说明:

  1. 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。
  2. join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

五、Process语法结构

Process([group [, target [, name [, args [, kwargs]]]]])
  • target:表示这个进程实例所调用对象;
  • args:表示调用对象的位置参数元组;
  • kwargs:表示调用对象的关键字参数字典;
  • name:为当前进程实例的别名;
  • group:大多数情况下用不到;

Process类常用方法:

  • is_alive():判断进程实例是否还在执行;
  • join([timeout]):是否等待进程实例执行结束,或等待多少秒;
  • start():启动进程实例(创建子进程);
  • run():如果没有给定target参数,对这个对象调用start()方法时,就将执行对象中的run()方法;
  • terminate():不管任务是否完成,立即终止;

Process类常用属性:

  • name:当前进程实例别名,默认为Process-N,N为从1开始递增的整数;
  • pid:当前进程实例的PID值;
from multiprocessing import Process
import os
from time import sleep

# 子进程要执行的代码
def run_proc(name, age, **kwargs):
    for i in range(10):
        print('子进程运行中,name= %s,age=%d ,pid=%d...' % (name, age,os.getpid()))
        print(kwargs)
        sleep(0.5)

        
if __name__ == '__main__':
    print('父进程 %d.' % os.getppid())
    p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
    print('子进程将要执行')
    p.start()
    sleep(1)
    p.terminate()
    p.join()
    print('子进程已结束')

进程编号的作用:

  • 进程的编号的目的是验证主进程和子进程的关系,可以得知子进程是由哪个主进程创建出来的。

获取进程编号的方式:

  1. 获取主(父)进程pid:os.getppid()
  2. 获取子进程pid:os.getpid()
  • 所有的子进程都来自于父进程,因此一个程序中的主进程编号得知,子进程编号按照主进程编号为起始值加一计算。
  • multiprocessing.current_process()方法获取当前的当前进程的详细信息(进程名称和进程编号)。

multiprocessing.current_process()方法获取当前的当前进程的详细信息(进程名称和进程编号)

from multiprocessing import Process
import time
import os

#两个子进程将会调用的两个方法
def  worker_1(interval):
    print("worker_1,父进程(%s),当前进程(%s)"%(os.getppid(),os.getpid()))
    t_start = time.time()
    time.sleep(interval) #程序将会被挂起interval秒
    t_end = time.time()
    print("worker_1,执行时间为'%0.2f'秒"%(t_end - t_start))


def  worker_2(interval):
    print("worker_2,父进程(%s),当前进程(%s)"%(os.getppid(),os.getpid()))
    t_start = time.time()
    time.sleep(interval)
    t_end = time.time()
    print("worker_2,执行时间为'%0.2f'秒"%(t_end - t_start))

#输出当前程序的ID
print("进程ID:%s"%os.getpid())

#创建两个进程对象,target指向这个进程对象要执行的对象名称,
#args后面的元组中,是要传递给worker_1方法的参数,
#因为worker_1方法就一个interval参数,这里传递一个整数2给它,
#如果不指定name参数,默认的进程对象名称为Process-N,N为一个递增的整数
p1=Process(target=worker_1,args=(2,))
p2=Process(target=worker_2,name="dongGe",args=(1,))

#使用"进程对象名称.start()"来创建并执行一个子进程,
#这两个进程对象在start后,就会分别去执行worker_1和worker_2方法中的内容
p1.start()
p2.start()

#同时父进程仍然往下执行,如果p2进程还在执行,将会返回True
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)

#join括号中不携带参数,表示父进程在这个位置要等待p1进程执行完成后,
#再继续执行下面的语句,一般用于进程间的数据同步,如果不写这一句,
#下面的is_alive判断将会是True,在shell(cmd)里面调用这个程序时
#可以完整的看到这个过程,大家可以尝试着将下面的这条语句改成p1.join(1),
#因为p2需要2秒以上才可能执行完成,父进程等待1秒很可能不能让p1完全执行完成,
#所以下面的print会输出True,即p1仍然在执行
p1.join()
print("p1.is_alive=%s"%p1.is_alive())

六、进程的创建-Process子类

创建新的进程还能够使用类的方式,可以自定义一个类,继承Process类,每次实例化这个类的时候,就等同于实例化一个进程对象,请看下面的实例:

from multiprocessing import Process
import time
import os

#继承Process类
class Process_Class(Process):
    #因为Process类本身也有__init__方法,这个子类相当于重写了这个方法,
    #但这样就会带来一个问题,我们并没有完全的初始化一个Process类,所以就不能使用从这个类继承的一些方法和属性,
    #最好的方法就是将继承类本身传递给Process.__init__方法,完成这些初始化操作
    def __init__(self,interval):
        Process.__init__(self)
        self.interval = interval

    #重写了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))

if __name__=="__main__":
    t_start = time.time()
    print("当前程序进程(%s)"%os.getpid())
    p1 = Process_Class(2)
    #对一个不包含target属性的Process类执行start()方法,就会运行这个类中的run()方法,所以这里会执行p1.run()
    p1.start()
    p1.join()
    t_stop = time.time()
    print("(%s)执行结束,耗时%0.2f"%(os.getpid(),t_stop-t_start))

七、Process中kill的方法

python语言中,我们可以使用os模块中的kill方法,根据pid杀死相应进程。

语法:os.kill(进程编号, 信号编号)

八、进程的特性

进程的特性介绍:

  1. 进程之间不共享全局变量
  2. 主进程会等待所有的子进程结束之后再结束

8.1 不共享全局变量

当一个进程对全局变量进行数据的修改,对于其他进程而言不会造成任何的影响,可以理解为每个进程都拿的是最初始的全局变量。或者可以理解为全局变量就是所谓资源,当创建一个进程,则系统会直接给这个进程里面复制一个全局变量。针对于这个全局变量而言,在进程之间都是相互独立存在的,之间没有任何的联系。

三个进程分别操作的都是自己进程内部的全局变量test_list,不会对其他的进程里面的全局变量造成影响,所以进程之间不共享全局变量。他们的关系只有一点,不同进程之间的全局变量的名字相同而已。

8.2 所有子进程结束主进程才会结束

主进程会等待所有的子进程执行结束之后才能结束

在主进程结束之前,手动结束了所有的子进程,那么程序的结束由主进程的结束来控制

如果需要实现主进程结束则整个程序结束:

  1. 在主进程结束之前,保证所有子进程结束使用子进程的terminate()
  2. 在子进程开启之前,设置当前子进程被被主进程守护,子进程的deamon属性设为true则意味着这个子进程被主进程守护,主进程结束守护结束,子进程也结束

九、单进程与多线程的优劣

9.1 单进程

默认程序运行创建一个进程。

一个Python文件运行,就是开启一个进程去处理。

进程中的场景:主线程去执行代码。

9.2 多进程

一个Python文件运行,占用一个进程去处理,假如同时要运行第二个Python文件,同样给第二个Python文件开启一个进程去处理。

多进程可以完成多任务,每个进程就好比一个独立车间,每个车间都各自在运营,每个进程也是各自在运行,执行各自的任务。

9.3 优劣对比

  1. 单进程开发简单;多线程开发复杂;
  2. 单进程在处理高并发时一般采用多启动进程的方式;多线程仅需启动多个线程。进程的切换开销比线程大;
  3. 多进程之间如果有信息通信则相对多线程效率较低,因为多线程属于同一地址空间的访问,效率相对较高(暂不涉及锁等一致性策略的影响);
  4. 多进程稳定好,一个进程死了不影响其他进程;多线程中,任意一个出现问题,将影响到所有。

本文地址:https://blog.csdn.net/qq_44034384/article/details/107885692

《浅谈Python多进程中的multiprocessing.doc》

下载本文的Word格式文档,以方便收藏与打印。