多进程传递进去的值没有生效。

2021-11-16 21:59:01 +08:00
 18870715400

from multiprocessing import Process
import time, random


class Example:
    pass


def fun(i, case):
    time.sleep(random.randint(1, 4))
    print("{},  age is {}".format(i, case.age))


def main():
    for i in range(3):
        setattr(Example, "age", i)
        print("satrt", Example.age)
        ps = Process(target=fun, args=(i, Example))
        ps.start()



if __name__ == '__main__':
    main()

代码如上所示, 但是最终在 fun 函数里面报了 AttributeError: type object 'Example' has no attribute 'age' 这个错误,setattr 没有生效, 请问一下大佬原因。

2286 次点击
所在节点    Python
14 条回复
minami
2021-11-16 22:07:16 +08:00
e = Example()
setattr(e, "age", i)
print("satrt", e.age)
ps = Process(target=fun, args=(i, e))
ps.start()

传一个实例进去
Nitroethane
2021-11-16 22:50:20 +08:00
`Example` 的类型是 `<class 'type'>`。刚试了下,3.9 对于 type 类型是传值,3.7.5 是传引用。因此你这个代码在 3.7.5 上是能正常跑的,但是在 3.9.6 上会报 AttritubeError 。
把代码改成这样,运行结果在两个版本上是不同的:

Nitroethane
2021-11-16 23:07:37 +08:00
@Nitroethane #2 请忽略这个回答,纯粹胡扯…… (被 bug 搞了一天,大脑处于混乱状态)。出现这个现象的原因是不同版本的 multiprocessing 库使用的默认的 start_method 导致的。3.9.7 上的 start_method 是 spawn ,而 3.7.5 是 fork ,所以 3.7.5 下面每次打印的 id 值是相同的。
18870715400
2021-11-16 23:51:29 +08:00
@Nitroethane 谢谢, 我看一下 fork 和 spawn 的区别.
18870715400
2021-11-16 23:52:01 +08:00
@minami 额, 就是因为不想传实例进去.
ClericPy
2021-11-17 00:13:30 +08:00
题外话:

最近踩坑挺多的, 并行计算还是尽量无状态, 纯函数越纯越好...
Nitroethane
2021-11-17 00:19:40 +08:00
@18870715400 #4 fork 模式的工作机制应该和 fork 系统调用类似,子进程和父进程的地址空间完全一致,因此引用的是同一个对象。
spawn 模式的工作机制应该和 execve 系统调用类似,用 fork 系统调用产生子进程后会用 execve 系统调用加载一个全新的 python 解释器实例,这时子进程和父进程的地址空间就不同了。不过这还不能解释,为什么给子进程传一个实例化的对象就没问题,我猜应该和内部的具体实现有关。
如果想快速解决这个错误的话,只需要在 main 函数最开始掉用一下 multiprocessing.set_start_method() 方法设置成 fork 。
Nitroethane
2021-11-17 00:21:56 +08:00
@ClericPy #6 同感,在 python 里搞多线程 /多进程纯粹是自讨苦吃。最近接手一个老项目,多进程套多线程,而且还是 python2 的,给我搞吐了。幸亏项目不大,迁移到 3.9 加部分重构花了三天时间
ClericPy
2021-11-17 00:31:49 +08:00
@Nitroethane

上一份工作维护老项目 Python2 是真挺头疼的

不过我现在上头下头都没别人, 所以选型时候我 all-in 协程了, 感觉算太香但还好. 随手写个流式下载上传, 把多进程和协程用上以后 CPU 利用率长时间 400%, 内存只有旧的 Java 版本的十分之一(似乎主要是流式传输占便宜...), 开发时间也确实才两三天. 就是交接工作时候不知道咋办了, 招不到玩协程的, 最多就是几个用过 gevent 猴子补丁, 头疼

现在想整轮子或者整个 Snippet 想办法利用多核时候少写几行代码, py 爹似乎也在关注这方向了, 子解释器还不知道有没有坑.

总之多进程时候我真是能不传变量就不传变量了, 貌似现在更建议通过通信来共享内存不去通过共享内存来通信, 多进程那些 Manager Queue Value Lock 什么的还是担心有坑
Nitroethane
2021-11-17 00:42:26 +08:00
@ClericPy 我主要是很长时间没写过 Python 了,一直写的 go ,而且以前写 Python 的时候学了一阵协程愣是没搞懂,然后就扔下了。直到最近翻了下流畅的 Python 里关于协程的部分才通透了。
我用多进程的时候就是 Manager 那一套,先创建 Manager ,然后用 Manager 创建 Queue ,通过这个 Queue 在进程和线程之间传递数据。等后面有时间再 all-in 协程了
dongyx
2021-11-17 00:52:16 +08:00
multiprocessing 模块有一个叫做 start_method 的全局模块属性。如果设置为 spawn ,那么启动的子进程会重置为一个干净的 Python 解释器并重新定位到要执行的函数,相当于于执行 fork+execv 系统调用然后执行你的函数。如果设置为 fork ,子进程会写时拷贝父进程的虚拟地址空间,类似于执行 fork 系统调用之后直接执行函数。

当 start_method 被设置为 spawn 的时候,Process 对象的 args 参数是通过 pickle 模块序列化之后跨进程传递给子进程的。而 pickle 在序列化类和函数的时候,仅仅是保存他们的完整名字引用。所以你可以认为你的 Example 类被序列化为一个类似于 b'__main__.Example'的字节序列。子进程通过这个名字定为 Example 类,而子进程是一个干净的解释器,所以 Example 类也是干净的。

在 UNIX 上,Python 默认使用 fork 模式,而在 Windows 上默认使用(也只支持) spawn 模式。但是有一点要特别注意,尽管 macOS 是一个 UNIX ,但是从 Python 3.8 开始,也以 spawn 作为默认的 start_method (但保持对 for 的支持)。

你可以选择在程序引入 multiprocess 模块后,通过执行`multiprocessing.set_start_method('fork')`来解决你的问题。
18870715400
2021-11-17 09:22:38 +08:00
@dongyx 好的, 谢谢大佬
haoliang
2021-11-17 14:54:09 +08:00
python 3.9.7, archlinux ; 运行正常
joApioVVx4M4X6Rf
2021-11-18 09:41:55 +08:00
闻到了坑的味道

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/815878

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX