1 - 进程 - Windows 10 - Python - multiprocessing - 简单多进程切换、进程传参、异步进程、守护进程(进程睡眠_堵塞和线程堵塞的区别)、主_子进程区分
@
测试环境:
操作系统: Window 10
工具:Pycharm
Python: 3.7
一、单进程
一般来说我们运行可执行文件,如脚本文件等,就相当于是在运行一个进程,系统会自动分配资源给这个文件运行,而这个进程就是父进程,或者说是主进程,跟线程差不多,有主线程和子线程,所以有了主进程,就应该有子进程。
举个例子:为了解决一个问题,想出了两个计划 A
和 B
,而且计划 A
和 B
都给了三次机会
代码演示:
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中写的,那在 Windows 的 Notebook 里加上
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: 大部分的基础数据类型,比如 list
,dict
都可以 pickle
,类似于 socket
,DB
connection
不可以,这一点有点类似 json
模块,可以序列化字典。
python一切皆对象,通常情况下不是所有的对象都是被序列化的。 举个例子,
sockets
对象,文件,数据库链接等。
任何从python来的内建类型都可以被序列化。你可以实现自定义的序列化代码,举个例子,存储数据库配置的连接并且在之后重连,你需要特殊的自定义逻辑来实现序列化。
所有的这些使得序列化比
xml
,json
,yaml
要强大的多(但是不可读)
————————————————————————————————————————