1 - 进程 - Windows 10 - Python - multiprocessing - 简单多进程切换、进程传参、异步进程、守护进程(进程睡眠_堵塞和线程堵塞的区别)、主_子进程区分

发布时间 2023-03-24 16:46:39作者: Loki_Severus

@

测试环境:

操作系统: Window 10
工具:Pycharm
Python: 3.7

一、单进程

一般来说我们运行可执行文件,如脚本文件等,就相当于是在运行一个进程,系统会自动分配资源给这个文件运行,而这个进程就是父进程,或者说是主进程,跟线程差不多,有主线程和子线程,所以有了主进程,就应该有子进程

举个例子:为了解决一个问题,想出了两个计划 AB ,而且计划 AB 都给了三次机会

代码演示:

import time
def A():
    for i in range(3):
        print(" A 计划……")
        time.sleep(1)
 
def B():
    for i in range(3):
        print(" B 计划……")
        time.sleep(1)
 
if __name__ == '__main__':
    A()
    B()
  

运行结果:
在这里插入图片描述
结果就是与A 计划 与 B 计划按顺序执行,这就是单进程,并且这个进程是主进程(父进程),这是传统的文件执行逻辑,单进程按顺序执行,但我们是想要尽可能的占用CPU资源,也就是说当前CPU的核心要尽可能的优先处理我们的进程,为我们的进程大开绿灯,也就是接下来要实现的多进程。

————————————————————————————————————————

二、简单多进程的实现

因为众所周知的GIL(全局解释器锁),在Python上实现真正的并发只能通过python 的multiprocessing。但是在Windows上,运用python 的 multiprocessing并没有在Linux那么简单。

多进程模块:

import multiprocessing

进程对象创建:

进程对象 = multiprocessing。Process(target=函数对象名, args=(元组元素,))

启动进程执行任务

进程对象.start()

主进程堵塞 —— 连接点 join

进程对象.join()

完整代码演示:

#from multiprocessing import process
import multiprocessing 
import time
    
def A():
    for i in range(3):
        print("A 计划……")
        time.sleep(1)
 
def B():
    for i in range(3):
        print("B 计划……")
        time.sleep(1)

if __name__ == "__main__":

    funA = multiprocessing.Process(target=A)
    funB = multiprocessing.Process(target=B)

    funA.start()
    funB.start()
	#funA = Process(target=A)
	#funB = Process(target=B)
	#funA.start()
    #funB.start()

运行结果:
在这里插入图片描述
这是在Pycharm运行的结果,在Windows的cmd命令终端,无法呈现这样的输出。
在这里插入图片描述
特别要注意的是,time.sleep()线程睡眠是会切换进程的,当子进程睡眠后,会切换到另外的子进程执行,有点类似线程的执行过程,不过这里是进程切换。

————————————————————————————————————————

三、简单多进程传参

multiprocessing.Process.py 源码:

class BaseProcess(object):
    '''
    Process objects represent activity that is run in a separate process

    The class is analogous to `threading.Thread`
    '''
    def _Popen(self):
        raise NotImplementedError

    def __init__(self, group=None, target=None, name=None, args=(), kwargs={},
                 *, daemon=None):
        assert group is None, 'group argument must be None for now'
        count = next(_process_counter)
        self._identity = _current_process._identity + (count,)
        self._config = _current_process._config.copy()
        self._parent_pid = os.getpid()
        self._popen = None
        self._closed = False
        self._target = target
        self._args = tuple(args)
        self._kwargs = dict(kwargs)
        self._name = name or type(self).__name__ + '-' + \
                     ':'.join(str(i) for i in self._identity)
        if daemon is not None:
            self.daemon = daemon
        _dangling.add(self)

可以看到target指执行函数,args指执行函数的参数,类型是元组kwargs是关键字传参,类型是字典

args 执行函数传参:

import multiprocessing
进程对象 = multiprocessing.Process(target=函数对象名,args=(参数,))

特别注意,若args的参数只有一个元素,那个逗号绝对不能省略的,否则传递的不是元组。

测试,有无逗号的区别:

>>> a = (("a"))
>>> a
'a'
>>> type(a)
<class 'str'>
>>> a = ((1))
>>> a
1
>>> type(a)
<class 'int'>
>>> a = (("a",))
>>> a
('a',)
>>> type(a)
<class 'tuple'>
>>>

多进程执行函数关键字传参kwargs的使用方式:

import multiprocessing
进程对象 = multiprocessing.Process(target=函数对象名,kwargs={"变量名": 变量值})

代码演示:

import multiprocessing 
import time
    
def A(num,name):   # num是运行次数
    for i in range(num):
        print("{}执行 A 计划……".format(name))
        time.sleep(1)
 
def B(num,name):
    for i in range(num):	# num是运行次数
        print("{}执行 B 计划……".format(name))
        time.sleep(1)

if __name__ == "__main__":
											# args参数
    funA = multiprocessing.Process(target=A,args=(3,"福尔摩斯")) 
    funB = multiprocessing.Process(target=B,kwargs={"num":3,"name":"诸葛亮"})
											# kwargs 关键字参数
    funA.start()
    funB.start()

运行结果:
在这里插入图片描述

这是在 Pycharm IDE运行的结果。

————————————————————————————————————————

四、获取多进程 id 编号

获取当前子进程的编号

import os
os.getpid()

获取当前进程的父进程的编号

import os
os.getppid()

完整代码演示:

import multiprocessing
import os
import time
    
def A(num,name):   # num是运行次数
    print("当前子进程的id: {}".format(os.getpid()))
    print("当前子进程的父进程id: {}".format(os.getppid()))
    for i in range(num):
        print("{}执行 A 计划……".format(name))
        time.sleep(1)
 
def B(num,name):
    print("当前子进程的id: {}".format(os.getpid()))
    print("当前子进程的父进程id: {}".format(os.getppid()))
    for i in range(num):    # num是运行次数
        print("{}执行 B 计划……".format(name))
        time.sleep(1)

if __name__ == "__main__":

    funA = multiprocessing.Process(target=A,args=(3,"福尔摩斯"))
    funB = multiprocessing.Process(target=B,kwargs={"num":3,"name":"诸葛亮"})

    funA.start()
    funB.start()

运行结果:
在这里插入图片描述
两个子进程的父进程都是一致的,同一个父进程。
系统会给子进程分配一个独一无二的进程 id

参考链接:(三)进程各种id:pid、pgid、sid、全局pid、局部pid

五、主进程会等待所有的子进程执行结束再结束。

这里看着像是子进程执行完毕了,在退出,而主进程早已执行完毕,所以主进程早就退出运行了的感觉,但其实不是的,在运行子进程时,主进程仍旧在运行着等待子进程的执行结束信号,然后再退出主进程运行。

import multiprocessing
import time
 
 
def B(name):
    for i in range(10):
        print("{}执行B 计划……".format(name))       
 
if __name__ == '__main__':

    funB = multiprocessing.Process(target=B, args=("诸葛亮",))
    funB.start()

    
    print("A 计划完毕……")
    

运行结果:
在这里插入图片描述

发现主进程显示已经完成了 A 计划,但是子进程还是在执行 B 计划,程序需等到子进程运行完,才算结束。

六、设置守护进程,当主进程结束时,子进程也不再继续执行,直接结束。

创建进程之后,加入这样一句代码

进程名称.daemon = True

这样子进程就会守护主进程,主进程结束,子进程也会自动销毁。

这句话这其实还不太完整,在注释掉了所有 time.sleep() 线程睡眠方法后,我发现守护进程完全没反应,根本就无法运行了,单单只运行主进程代码,子进程没有执行 B 计划方法,所以要想使用守护进程,除了使用 daemon 属性外,还得和 time.sleep() 线程睡眠方法 组合使用,否则守护进程仅仅只是一个为了服务而创建出来的进程,却没有给它权力,而 time.sleep() 线程睡眠方法,就有能力切换进程运行,如何理解进程堵塞和线程堵塞的区别,可以先看进程堵塞和线程堵塞的参考链接:

终于想明白操作系统的中断和进程阻塞了

4 - 线程 - CPython - 理解伪多线程中 join() 线程连接点(主线程堵塞) 和 sleep() 线程睡眠 的作用

代码演示:

import multiprocessing
import time

def B(name):

    for i in range(10):  # 执行 10 次 B 计划
        print("{}执行B 计划……".format(name))
        time.sleep(0.5)

if __name__ == '__main__':
    funB = multiprocessing.Process(target=B, args=("诸葛亮",))
    # 设置进程守护
    funB.daemon = True
    funB.start()
    time.sleep(3)
    

运行结果:
在这里插入图片描述
结果显示本来应该执行 10 次 执行 B 计划的,但是在执行了 6 次 B 计划,就因为父进程 A 计划执行结束了,子进程 B 计划就跟着退出执行。

————————————————————————————————————————

七、关于多进程必须加上 if __name__ == "__main__" 的理由(进程区分):

因为众所周知的GIL(全局解释器锁),在Python上实现真正的并发只能通过python的multiprocessing。但是在Windows上,运用python的multiprocessing并没有在Linux那么简单。

if __name__ == "__main__":

也许大家有些在Linux跑的很好的多进程的程序,在Windows上一跑就会经常遇到这些错误的信息

根本原因在与 Windows 的进程启动的方式和 Linux 是不一样的。

Windows的进程启动方式是Spawn,Linux的缺省的启动方式是Fork。简单的说,Fork会复制父进程的所用东西,而Spawn不会。对于Python而言,Spawn会在进程中生成一个新的Python解释器,并重新加载各个module.

也就是说每次多开一个进程,那个新开的进程,就得弄一个新的 Python解释器,并且还要加载各种原Python环境下的各种模块,而且还要加载当前要运行的 py脚本文件,把自己当成了一个独立的主进程去运行,这不是乱来吗?你主进程运行了一次代码,子进程还要继续运行一次,那子进程因为包含了主进程创建子进程的代码,所以又得创建一个子进程,然后又创建新的 Python 解释器,就这样无限循环下去,电脑不得崩溃?

如下所示:

#test.py
import multiprocessing
import time

def A():
    for i in range(3):
        print("A 计划……")
        time.sleep(1)

def test():
	funA = multiprocessing.Process(target=A)
	funA.start()

test()

运行一下就会报错,Windows电脑不会让子线程继续无限循环下去的,会报错的。

在Windows下,进程的启动方式是spawn,子进程需要先import test.py这个module(也就是要运行的py脚本文件),除了这个脚本文件外,还会导入全局python环境下的模块包,在import的过程中,test()就在子进程中运行,然后子进程又会产生新的进程(funA=Process(target=A,args=[q]))当然 Windows 不会让这种死循环产生,所以发现这种情况就会抛出开头的异常。
所以在Windows的环境下 if __name__ == "__main__" 必须被加上保证新的进程不会在import module的时候产生。

这一段代码的作用在于标识主函数(主进程),令子进程不用运行if__name__ == "__main__"下面的代码。

因此,在 Windows环境下多进程文件内加上 if__name__ == "__main__" 的作用之大就不言而喻了,当然我觉得要养成习惯,无论是在 Windows 还是 类 Unix 系统(Linux)环境下,运行的py脚本文件内都应该加上这一段代码,来区分父进程与子进程。

| start method | fork | spawn |
|--------| -------------|--------| -------------|
| 进程启动时 import module | NO | YES |
| 变量的ID与父进程保持一致(具体的看链接) | YES | NO |
| 子进程能得到在 if __name__ == "__main__" block中定义的模块 |YES | NO |

参考链接:
创建子进程时变量的地址与父进程一样而数值不一样的问题

linux进程系列(3)父子进程变量虚拟内存地址相同但变量值不同的问题

————————————————————————————————————————————————————

上面的 block 指的是代码块,也就是说在类 Unix 系统(Linux)下,python多进程是可以调用if __name__ == "__main__"下定义的相关模块,但是不会 import 当前py脚本文件,重复执行,无限循环,而是会在初始执行函数下面继续运行调用其他的函数模块,比如上面 test() 函数执行后的其他模块。

————————————————————————————————————————

八、Jupyter Notebook 与 进程区分的关联

有相当多的python 代码是在Notebook中写的,那在 WindowsNotebook 里加上 if __name__ == "__main__", 也能正常的的运行多进程的程序吗?答案是否定的, 不行!!!

因为python有两种运行方式,一种是script(脚本模式),另一种是interactive(交互式模式) 。
interactive 模式下, 子进程没有执行import父进程的module
结果就是,子进程找不到 执行函数了,

def B(): 
		print("This is plan B!!!")

会报错 B is not defined.

Windows 环境的交互式模式下,本来是要用 spawn 打开新的进程,并导入父进程的模块 module ,其中还包括了运行 py 脚本文件模块名,可是交互式模式下却没有执行导入模块这一步骤,所以自然子进程就仅仅只是打开了进程,传递了执行函数名,但却没有找到执行函数的函数体,所以才报没有定义的程序错误。

九、关于多进程与GPU的关系

Python是机器学习的主要语言,机器学习特别是深度学习经常需要在GPU进行编程

同时在python多进程中传递的数据必须是可以通过pickable来进行序列化的,也就是必须是pickable的,而GPU上的数据是不可以pickable的,如果传递给子进程一个再GPU上的变量,python会报unpickable的异常。

所以在多进程中传递数据,必须是CPU上的变量,然后在进程中再把这些数据加载到GPU上。

也就是说先在CPU中传递多进程的数据,然后利用CPU的能力,再将它们传递给GPU。

Windows 没有 fork,要像 Linux 那种 fork 就只能用 pickle 拷贝整个进程;

python 中可序列化的条件是什么样的?

Answer: 大部分的基础数据类型,比如 listdict 都可以 pickle,类似于 socketDB connection不可以,这一点有点类似 json 模块,可以序列化字典。

python一切皆对象,通常情况下不是所有的对象都是被序列化的。 举个例子,sockets对象,文件,数据库链接等。
任何从python来的内建类型都可以被序列化。

你可以实现自定义的序列化代码,举个例子,存储数据库配置的连接并且在之后重连,你需要特殊的自定义逻辑来实现序列化。

所有的这些使得序列化比xml,json,yaml要强大的多(但是不可读)

————————————————————————————————————————

参考链接

Python多进程

Windows上的Python多进程

python picklable