Python-多进程踩坑实记

Created on 2020-01-04

@author 假如我年华正好

参考资料:

  • multiprocessing模块源码:https://github.com/python/cpython/tree/3.6/Lib/multiprocessing
  • multiprocessing模块官方文档:https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing

1. 关于多线程和多进程的一些知识要点

  • 并发 VS 并行:

    • 并发:假的多任务(CPU核数 < 任务数)
    • 并行:真的多任务(CPU核数 >= 任务数)
  • 多线程模块:threading;多进程模块:multiprocessing

  • 每一个应用程序都有一个自己的进程。操作系统会为这些进程分配一些执行资源,例如内存空间等。

  • 每一个进程至少有一个线程,多个线程共享这些内存空间,并由操作系统调用,以便并行计算。

  • 进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响

  • 而线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉

  • 多线程可以直接共享全局变量,但是要注意资源竞争的问题;

  • ⭐而进程没有任何共享状态,进程修改的数据,改动仅限于该进程内 ,,多进程间的通信比较麻烦。

2. 太长不看版之本文的填坑总结

  • python 的多进程在 windows 下存在一些奇怪的坑,还是 Linux 大法好。

  • 子进程在 Spyder、jupyter 等IDE下运行时是没有输出的,就算报错了也不知道,,

  • 在 pycharm 和 cmd 中可以看到输出结果,,

  • 在 cmd (shell)中执行 python 脚本的命令为:python xxx.py (需要先切换至.py所在目录)。

  • 执行子进程的代码必须要放在if __name__ == "__main__":语句后面,否则子进程不执行!

  • 但是创建子进程的语句可以放在前面。

  • 多线程之间共享全局变量,但是多进程之间是不共享的,,多进程之间的通信比较麻烦,,

  • 尤其是想共享类的实例对象的时候,需要自定义 Manager() 对象,,

  • Manager() 创建的代理对象可以直接调用类的方法,却不可以直接读取类的属性!!

  • 如果要获取类的属性,需要通过在类中定义相应的方法!!

  • 可以在子进程调用的函数里打印子进程的id:print("in process pid=%d" % (os.getpid(), ))

  • windows 在 shell 中查看当前python进程:wmic process where name="python.exe"

本文的代码列表:

  1. # 3.1 版本一:测试子进程输出的错误示范❌

  2. # 3.2 版本二:测试子进程输出的正确示范✔

  3. # 4.2-(1)测试1:一个子进程

  4. # 4.2-(1) 测试2:两个子进程

  5. # 4.2-(1)测试3:很多子进程

  6. # 4.2-(1) 测试4:不创建代理对象,直接定义全局变量,是不共享的

  7. # 4.2-(2) 测试1:用代理对象可直接调用类方法

  8. # 4.2-(2) 测试2:用代理对象无法直接读取类属性

  9. # 4.2-(2)测试3:多进程共享类的实例对象

3. 坑:多进程在Windows下运行没有输出

没有输出可能情况:

  1. 运行了,但是没有打印结果

  2. 压根没运行

下面用两个版本的py脚本进行测试:

3.1 版本一测试

运行结果

  • 在spyder中:主进程很快结束,子进程没有任何输出和反馈;
  • 在jupyter notebook中:主进程很快结束,子进程没有任何输出和反馈;
  • 在 shell 中:主进程很快结束,但子进程报错;
  • 在 pycharm 中:同shell

结果分析

  • 主进程很快结束,可见子进程没有执行

  • shell 和 pycharm 会打印子进程的报错信息,,

  • 而 spyder 和 jupyter notebook 不会打印子进程的情况,报错了也不打印,所以我们无法知道子进程到底有没有在执行。

以下是代码和结果:

# 3.1 版本一:测试子进程输出的错误示范❌
import multiprocessing
import time

#创建一个简单进程每隔x秒打印时间一次
def clock(wait_time):
    i = 0
    while i<10:
        print("now is %s" %time.ctime())
        time.sleep(wait_time)
        i += 1
        
p = multiprocessing.Process(target=clock, args=(5,))
print("开始运行子进程...")
p.start()
p.join()
print("子进程运行结束!")
开始运行子进程...
子进程运行结束!

以下是在 shell 和 pycharm 中的输出:

(base) D:\github\CategoricalEncoder\scr>python test3.py
开始运行子进程...
开始运行子进程...
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\spawn.py", line 105, in spawn_main
    exitcode = _main(fd)
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\spawn.py", line 114, in _main
    prepare(preparation_data)
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\spawn.py", line 225, in prepare
    _fixup_main_from_path(data['init_main_from_path'])
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\spawn.py", line 277, in _fixup_main_from_path
    run_name="__mp_main__")
  File "C:\Users\admin\Anaconda3\lib\runpy.py", line 263, in run_path
    pkg_name=pkg_name, script_name=fname)
  File "C:\Users\admin\Anaconda3\lib\runpy.py", line 96, in _run_module_code
    mod_name, mod_spec, pkg_name, script_name)
  File "C:\Users\admin\Anaconda3\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "D:\github\CategoricalEncoder\scr\test3.py", line 20, in <module>
    p.start()
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\process.py", line 112, in start
    self._popen = self._Popen(self)
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\context.py", line 223, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\context.py", line 322, in _Popen
    return Popen(process_obj)
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\popen_spawn_win32.py", line 33, in __init__
    prep_data = spawn.get_preparation_data(process_obj._name)
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\spawn.py", line 143, in get_preparation_data
    _check_not_importing_main()
  File "C:\Users\admin\Anaconda3\lib\multiprocessing\spawn.py", line 136, in _check_not_importing_main
    is not going to be frozen to produce an executable.''')
RuntimeError:
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.
子进程运行结束!

3.2 版本二测试

增加 if __name__ == "__main__": 语句

运行结果:

  • 在spyder中:主进程等了好一会才结束,子进程依然没有任何输出;
  • 在jupyter notebook中:主进程很快结束,子进程没有任何输出和反馈;
  • 在 shell 中:主进程、子进程正常打印输出;
  • 在 pycharm 中:同shell

结果分析

  • jupyter notebook 中主进程很快结束,说明子进程依然没有执行——因为jupyter notebook 中if __name__ == "__main__":语句并不生效。

  • spyder 中可以正常执行子进程了,但是无法打印子进程的输出。——所以此时虽然我们知道子进程在执行了,但还是看不到具体的执行情况。

  • shell 和 pycharm 中主进程、子进程正常执行和打印。

以下是代码和结果:

# 3.2 版本二:测试子进程输出的正确示范✔
import multiprocessing
import time

#创建一个简单进程每隔x秒打印时间一次
def clock(wait_time):
    i = 0
    while i<10:
        print("now is %s" %time.ctime())
        time.sleep(wait_time)
        i += 1

if __name__ == "__main__":        
    p = multiprocessing.Process(target=clock, args=(5,))
    print("开始运行子进程...")
    p.start()
    p.join()
    print("子进程运行结束!")
开始运行子进程...
子进程运行结束!

以下是在 shell 和 pycharm 中的输出:

(base) D:\github\CategoricalEncoder\scr>python test3.py
开始运行子进程...
now is Fri Jan  3 23:16:06 2020
now is Fri Jan  3 23:16:11 2020
now is Fri Jan  3 23:16:16 2020
now is Fri Jan  3 23:16:21 2020
now is Fri Jan  3 23:16:26 2020
now is Fri Jan  3 23:16:31 2020
now is Fri Jan  3 23:16:36 2020
now is Fri Jan  3 23:16:41 2020
now is Fri Jan  3 23:16:46 2020
now is Fri Jan  3 23:16:51 2020
子进程运行结束!

3.3 结论:

python进程在Win下运行有诸多限制,

  • 🍎第一,子进程在Spyder、jupyter等IDE下运行时是没有输出的,就算报错了也不知道,

    • —— 因为IDE输出窗口输出的是主进程的内容,我们创建的子进程是无法输出的 (来自yungeisme

    • —— 因为 multiprocessing 模块在交互模式是不支持的 (来自 chenpe32cp

    • 要看输出结果,要在cmd下运行python脚本,命令为:python xxx.py

  • 🍎第二,执行子进程的代码必须要放在if __name__ == "__main__":语句后面,否则子进程不执行! (下面是本人亲测得出的结论:)

    • 创建语句可以放在前面,例如把 p = multiprocessing.Process(target=clock, args=(5,)) 放到前面,子进程还是可以正常执行;(所以来自yungeisme的限制二的表述应该是有误的)
    • 执行语句必须放在后面!例如如果把p.start()也放到前面,子进程就会报错了!

4. 巨坑:多进程之间的通信

多线程之间共享全局变量,但是多进程之间是不共享的,,

多进程之间的通信比较麻烦,,

尤其是共享类实例的时候!!搜索相关的内容好少,,这里记录一下摸索了两天的一些收获,

首先感谢参考:https://blog.csdn.net/jacke121/article/details/82630693

4.1 一些知识点的补充

(1). Exchanging objects between processes

参考文档

进程间可以交换对象,即一个进程可以发送东西出去,另一个进程可以接收东西,,
multiprocessing 模块提供了支持进程间进行通信的方法:

  1. Queue
  2. Pipes
(2). Synchronization between processes

参考文档

保持进程间的同步:

  • 同多线程的threading模块一样,可以用锁:Lock

不用锁的话,多个进程的结果和输出可能会混淆在一起。

(3). Sharing state between processes

参考文档

进程间共享状态

(其实,在并行运算时,通常最好尽可能避免共享状态。多进程时尤其如此。实在需要共享的话,multiprocessing也提供了一些方法)

  1. Shared memory:共享内存,使用 ValueArray

    • These shared objects will be process and thread-safe.
  2. Server process:

    • Manager:服务器进程,代理对象(Proxy Objects)
    • support types list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array

1. Shared memory VS 2. Server process

  • 2.Server process managers支持任意对象,比1.Shared memory更灵活;
  • 2.支持通过网络在不同电脑之间共享;
  • 但是2比1更慢。

4.2 多进程用 Manager() 共享状态

(1) Manager() 共享:列表,字典,值

Manager()创建代理对象,代理对象可以被多个进程共享。例如:

  • Manager().list() ——列表
  • Manager().dict() ——字典
  • Manager().Value() ——值
# 4.2-(1)测试1:一个子进程
from multiprocessing import Process, Manager

def f(d, l, v):
    d[1] = 'a'
    l.reverse()
    v.value += 1

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
        l = manager.list(range(5))
        v = manager.Value('v', 0)

        p = Process(target=f, args=(d, l, v))
        p.start()
        p.join()

        print(d)
        print(l)
        print(v.value)
输出结果为:
{1: 'a'}
[4, 3, 2, 1, 0]
1
# 4.2-(1) 测试2:两个子进程
from multiprocessing import Process, Manager

def f1(d, l, v):
    d[1] = 'a'
    l.reverse()
    v.value += 1
    
def f2(d, l, v):
    d[2] = 'b'
    l.append(1000)
    v.value += 1

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
        l = manager.list(range(5))
        v = manager.Value('v', 0)

        p1 = Process(target=f1, args=(d, l, v))
        p1.start()

        p2 = Process(target=f2, args=(d, l, v))
        p2.start()

        p1.join()
        p2.join()

        print(d,l,v.value)
输出结果为:
{1: 'a', 2: 'b'} [4, 3, 2, 1, 0, 1000] 2
# 4.2-(1)测试3:很多子进程
import os
from multiprocessing import Process, Manager

def f(v):
    v.value += 1
    print("in process pid=%d ,v=%s" % (os.getpid(), v.value)) #打印子进程信息

if __name__ == '__main__':
    with Manager() as manager:
        v = manager.Value('v', 0)

        ps = [Process(target=f, args=(v,)) for i in range(10)]
        for p in ps: 
            p.start()

        for p in ps:
            p.join()

        print(v.value)

输出结果为:

in process pid=348 ,v=1
in process pid=11124 ,v=2
in process pid=20084 ,v=3
in process pid=13924 ,v=4
in process pid=984 ,v=5
in process pid=2860 ,v=6
in process pid=7776 ,v=7
in process pid=2276 ,v=8
in process pid=11380 ,v=9
in process pid=6696 ,v=10
10

如果不用Manager()创建代理对象,直接定义全局变量,子进程之间是不共享的:

# 4.2-(1) 测试4:不创建代理对象,直接定义全局变量,是不共享的
import os
from multiprocessing import Process

def f(v):
    v += 1
    print("in process pid=%d ,v=%s" % (os.getpid(), v))

if __name__ == '__main__':
    v = 0

    ps = [Process(target=f, args=(v,)) for i in range(10)]
    for p in ps: 
        p.start()

    for p in ps:
        p.join()

    print(v)

输出结果为:

in process pid=15476 ,v=1
in process pid=5848 ,v=1
in process pid=16452 ,v=1
in process pid=12944 ,v=1
in process pid=15276 ,v=1
in process pid=18448 ,v=1
in process pid=5272 ,v=1
in process pid=16496 ,v=1
in process pid=7212 ,v=1
in process pid=8652 ,v=1
0
(2) Manager() 共享:类的实例对象

如果要共享类的实例对象,需要 Customized managers:

——creates a subclass of BaseManager and uses the register() classmethod to register new types or callables with the manager class.

自定义代理对象的一般使用

具体流程如下:

# 导入模块
from multiprocessing.managers import BaseManager

# 定义需要共享的类
class myClass:
    def __init__(self):
        # ...
        pass
    def foo(self):
        #...
        pass
    
# 新建 BaseManager 子类,空的就行
class MyManager(BaseManager):
    pass

# 把需要共享的类注册到 MyManager 里
# 第二个参数是个可调用对象(例如类),第一个参数则是之后用来引用该对象的名字
MyManager.register('myClassName', myClass) 

# 使用
if __name__ == "__main__":
    with MyManager() as manager:
        obj = manager.myClassName() # 注意这里要用myClassName,而不是myClass
        # 代理对象调用类方法
        obj.foo()

注意:

  • 代理对象可以直接调用类方法,,

  • 但是不能直接读取类属性!!

    • ——直接读取会报错:AttributeError: 'AutoProxy[xxx]' object has no attribute 'xxx'
    • ——需要通过在类中写一个获取类属性的方法来读取类属性

以下是测试例子:

# 4.2-(2) 测试1:用代理对象可直接调用类方法

from multiprocessing.managers import BaseManager

class MathsClass:
    def add(self, x, y):
        return x + y
    def mul(self, x, y):
        return x * y

class MyManager(BaseManager):
    pass

MyManager.register('Maths', MathsClass)

if __name__ == '__main__':
    with MyManager() as manager:
        maths = manager.Maths()
        print(maths.add(4, 3))         # prints 7
        print(maths.mul(7, 8))         # prints 56
# 4.2-(2) 测试2:用代理对象无法直接读取类属性
from multiprocessing.managers import BaseManager

class MathsClass:
    def __init__(self):
        self.value = 0
    
    #==============================================
    # 在类内写一个读取类属性的方法 
    def get_value(self): 
        return self.value
    #==============================================
        
    def add(self, x, y):
        self.value += 1
        return x + y
    def mul(self, x, y):
        self.value += 1
        return x * y

class MyManager(BaseManager):
    pass

MyManager.register('Maths', MathsClass)

if __name__ == '__main__':
    with MyManager() as manager:
        maths = manager.Maths()
        # 直接读取会报错:AttributeError: 'AutoProxy[Maths]' object has no attribute 'value'
        #print(maths.value) 
        # 要通过调用方法来读取
        print(maths.get_value())       # prints 2
自定义代理对象在多进程中的使用

具体流程如下:

# 导入模块
from multiprocessing.managers import BaseManager

# 定义需要共享的类
class myClass:
    def __init__(self):
        # do something...
        pass
    def foo(self):
        # do something...
        pass
    
# 新建 BaseManager 子类,空的就行
class MyManager(BaseManager):
    pass

# 把需要共享的类注册到 MyManager 里
# 第二个参数是个可调用对象(例如类),第一个参数则是之后用来引用该对象的名字
MyManager.register('myClassName', myClass) 

# 定义 Manager2,用来启动 MyManager
def Manager2():
    m = MyManager
    m.start()
    return m

# 定义子进程要调用的函数,在函数中放类方法
# 如果多个子进程存在资源竞争问题,则要加锁;确定不存在也可以不加
def func(obj, lock):
    with lock:
        obj.foo() # 调用 myClass类中的方法
        # do something another...
        
# 使用
if __name__ == "__main__":
    with MyManager() as manager:
        obj = manager.myClassName() # 注意这里要用myClassName,而不是myClass
        # 创建锁
        lock = Lock()
        # 创建子线程,执行函数 func
        process = [Process(target=func, args=(obj, lock)) for i in range(3)]
        for p in process:
            p.start()
        for p in process:
            p.join()
        # do something another...

注意:用with MyManager() as manager:比直接用manager = Manager2()更安全。

# 4.2-(2)测试3:多进程共享类的实例对象
# -*- coding: utf-8 -*-
from multiprocessing import Process, Value, Lock
from multiprocessing.managers import BaseManager
import time
import os
 
class Employee(object):
    def __init__(self, name, salary):
        self.name = name
        #self.salary = Value('i', salary)
        self.salary = salary
        self.data=[]

    def increase(self):
        self.name = self.name + self.name[0]
        #self.salary.value += 100
        self.salary += 100
        self.data.append(self.salary)
        
    def getInfo(self):
        #return self.name + ':' + str(self.salary.value) + str(self.data)
        return self.name + ':' + str(self.salary) + str(self.data)

 
class MyManager(BaseManager):
    pass

 
def Manager2():
    m = MyManager()
    m.start()
    return m

 
MyManager.register('Employee', Employee)

 
def func(em, lock):
    with lock:
        time.sleep(1)
        em.increase()
        print("in process pid=%d:%s" % (os.getpid(), em.getInfo()))


if __name__ == '__main__':
    with Manager2() as manager:        
        em = manager.Employee('小a', 0)
        lock = Lock()
        proces = [Process(target=func, args=(em, lock)) for i in range(3)]
        for p in proces:
            p.start()
        for p in proces:
            p.join()
        print(em.getInfo())

输出结果为:

in process pid=3676:小a小:100[100]
in process pid=15568:小a小小:200[100, 200]
in process pid=11812:小a小小小:300[100, 200, 300]
小a小小小:300[100, 200, 300]

5. 查看当前 python 进程

参考来源:Linux 和 Windows 查看当前运行的 python 进程及 GPU、CPU、磁盘利用率

Linux

在 shell 中执行如下指令:

ps -ef | grep python

或者

ps aux | grep python

Windows

在 shell 中执行如下指令:

wmic process where name="python.exe"

或者

wmic process where name="python.exe" list full
wmic process where name="python.exe" list brief

查看其它程序进程,只需要将 python 改个名即可。

--------完--------

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值