【问题标题】:Python multiprocessing making same object instance for every processPython多处理为每个进程创建相同的对象实例
【发布时间】:2021-11-04 07:01:11
【问题描述】:

我写了一个简单的例子来说明我到底要敲什么。可能有一些非常简单的解释,我只是想念。

import time
import multiprocessing as mp
import os


class SomeOtherClass:
    def __init__(self):
        self.a = 'b'


class SomeProcessor(mp.Process):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        soc = SomeOtherClass()
        print("PID: ", os.getpid())
        print(soc)

if __name__ == "__main__":
    queue = mp.Queue()

    for n in range(10):
        queue.put(n)

    processes = []

    for proc in range(mp.cpu_count()):
        p = SomeProcessor(queue)
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

结果是:

PID: 11853
<__main__.SomeOtherClass object at 0x7fa637d3f588>
PID: 11854
<__main__.SomeOtherClass object at 0x7fa637d3f588>
PID: 11855
<__main__.SomeOtherClass object at 0x7fa637d3f588>
PID: 11856
<__main__.SomeOtherClass object at 0x7fa637d3f588>

对象地址对所有人来说都是相同的,无论每次初始化都发生在新进程中。 任何人都可以指出什么问题。谢谢。

我也想知道这种行为,当我第一次在主进程中初始化同一个对象,然后在其上缓存一些值,然后在每个进程上初始化同一个对象时。然后进程继承主进程对象。

import time
import multiprocessing as mp
import os
import random

class SomeOtherClass:

    c = {}

    def get(self, a):
        if a in self.c:
            print('Retrieved cached value ...')
            return self.c[a]

        b = random.randint(1,999)

        self.c[a] = b

        return b


class SomeProcessor(mp.Process):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        pid = os.getpid()
        soc = SomeOtherClass()
        val = soc.get('new')
        print("Value from process {0} is {1}".format(pid, val))

if __name__ == "__main__":
    queue = mp.Queue()

    for n in range(10):
        queue.put(n)

    pid = os.getpid()
    soc = SomeOtherClass()
    val = soc.get('new')
    print("Value from main process {0} is {1}".format(pid, val))

    processes = []

    for proc in range(mp.cpu_count()):
        p = SomeProcessor(queue)
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

这里的输出是:

Value from main process 13052 is 676
Retrieved cached value ...
Value from process 13054 is 676
Retrieved cached value ...
Value from process 13056 is 676
Retrieved cached value ...
Value from process 13057 is 676
Retrieved cached value ...
Value from process 13055 is 676

【问题讨论】:

  • 很抱歉没有提及。输出此结果的 Python 的确切版本是 3.6.9
  • 你为什么认为这是个问题?
  • 这里没有问题。实例在不同的进程中,不共享状态。
  • @AKX 我可以证明你错了。这正是我问这种行为的原因。
  • @MarioKirov 您将SomeOtherClass.c 声明为类级变量。它也将在同一进程中的所有SomeOtherClassinstances 之间共享。 (如果您希望它是实例级的,则需要在 __init__ 中执行 self.c = {}。)通过分叉,相同的值也将出现在子进程中。

标签: python multiprocessing python-multiprocessing python-object


【解决方案1】:

扩展 cmets 和讨论:

  • 在 Linux 上,multiprocessing 默认为 fork 启动方法。分叉一个进程意味着子进程将共享父进程数据的写入时复制版本。这就是为什么全局创建的对象在子进程中具有相同的地址。
    • 在 macOS 和 Windows 上,默认启动方法是 spawn - 在这种情况下不共享任何对象。
  • 子进程将在写入对象后立即拥有其唯一的对象副本(事实上,在 CPython 内部,当它们甚至读取它们时,由于引用计数器位于对象头)。
  • 定义为的变量
    class SomeClass:
        container = {}
    
    是类级别,而不是实例级别,将在SomeClass 的所有实例之间共享。那是,
    a = SomeClass()
    b = SomeClass()
    print(a is b)  # False
    print(a.container is b.container is SomeClass.container)  # True
    a.container["x"] = True
    print("x" in b.container)  # True
    print("x" in SomeClass.container)  # True
    
    由于类的状态被分叉到子进程中,共享的container 似乎也是共享的。但是,在子进程中写入容器将不会出现在父进程或兄弟进程中。只有某些特殊的 multiprocessing 类型(以及某些较低级别的原语)可以跨越进程边界。
  • 要在实例和进程之间正确分离container,它需要是实例级别的:
    class SomeClass:
        def __init__(self):
            self.container = {}
    
    (当然,如果一个SomeClass 被全局实例化,并且一个进程被分叉,那么它在分叉时的状态将在子进程中可用。)

【讨论】:

  • 解释得很好。再次感谢您深入而简单的解释。
  • 我不确定它是否完全解释了 OP 看到的内容。我同意在 fork 时,分叉进程中的所有对象都将具有与主进程相同的地址。但是在 OP 的情况下,每个进程现在都在继续运行,并且每个进程都在创建 new 对象。正是每个地址空间中的这些新对象具有相同的地址。现在您可以争辩说,每个分叉的进程都在以相同的方式进行,而 就是这些对象将具有相同地址的原因。但你没有争辩。如果内存分配使用随机数生成器,您将不会看到这一点。
【解决方案2】:

查看修改后的代码,显示每个SomeOtherClass 都是不同的。

import time
import multiprocessing as mp
import os


class SomeOtherClass:

  def __new__(cls, *args, **kwargs):
        print('-- inside __new__ --')
        return super(SomeOtherClass, cls).__new__(cls, *args, **kwargs)


    def __init__(self):
        self.a = os.getpid()
    def __str__(self):
        return f'{self.a}'


class SomeProcessor(mp.Process):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        soc = SomeOtherClass()
        print("PID: ", os.getpid())
        print(soc)

if __name__ == "__main__":
    queue = mp.Queue()

    for n in range(10):
        queue.put(n)

    processes = []

    for proc in range(mp.cpu_count()):
        p = SomeProcessor(queue)
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

输出

 -- inside __new__ --
PID:  25054
25054
-- inside __new__ --
PID:  25055
25055
-- inside __new__ --
PID:  25056
25056
-- inside __new__ --
PID:  25057
25057
-- inside __new__ --
PID:  25058
25058
-- inside __new__ --
PID:  25059
25059
-- inside __new__ --
PID:  25060
25060
-- inside __new__ --
PID:  25061
25061

【讨论】:

  • 对与错。他们确实处于不同的过程中。但我想要完成的是在不同的进程上拥有一个不同的新实例。
  • `不同进程上的相同实例` - 这不是这种情况,因为我们有明确的证据表明__init__ 被调用了 N 次。不是吗?
  • @MarioKirov 它们不同的实例,因为它们位于不同的地址空间。他们只是碰巧有相同的地址。我猜你是在Linux下运行的。如果你在 Windows 下运行,你可能不会看到这个。只是猜测。当我在 Linux 下运行它时,我得到了相同的地址,但在 Windows 下却没有。 Linux 使用 forkspawn 为 Windows 创建进程。
  • 我添加了__new__ 实现以说服您它不是同一个实例。看代码。
  • @balderman 好的,在这种情况下你是对的,它们是不同的,我的错,只是因为我删掉了完整的例子。现在将其添加到第一个问题下方的问题中。谢谢
【解决方案3】:

tldr:它们实际上不是同一个实例,所以不用担心。

嗯,这很有趣。它们的内存引用完全相同,但实例肯定不同。如果我们这样修改代码:

import time
import multiprocessing as mp
import os


class SomeOtherClass:
    def __init__(self, num):
        self.a = num  # <-- Let's identify the instance with the pid
    
    def __str__(self):
        return f"I'm number {self.a}"  # <-- Better representation of the instance


class SomeProcessor(mp.Process):
    def __init__(self, queue):
        super().__init__()
        self.queue = queue

    def run(self):
        soc = SomeOtherClass(os.getpid())  <-- Use the PID to instantiate different objects
        print("PID: ", os.getpid())
        print(soc)
        time.sleep(1)
        print(soc)  # <-- Give it a second and print again

if __name__ == "__main__":
    queue = mp.Queue()

    for n in range(10):
        queue.put(n)

    processes = []

    for proc in range(mp.cpu_count()):
        p = SomeProcessor(queue)
        p.start()
        processes.append(p)

    for p in processes:
        p.join()

我们可以看到实例肯定是不同的,它们并没有被修改,因为在time.sleep()之后它们的属性仍然没有改变:

PID:  668424
I'm number 668424
PID:  668425
I'm number 668425
PID:  668426
I'm number 668426
...
I'm number 668435
I'm number 668424
I'm number 668426
...

然而,如果我们删除 __str__ 函数,我仍然会看到相同的内存引用:

<__main__.SomeOtherClass object at 0x7f3e08d83bb0>
PID:  669008
<__main__.SomeOtherClass object at 0x7f3e08d83bb0>
PID:  669009
<__main__.SomeOtherClass object at 0x7f3e08d83bb0>
PID:  669010
...
<__main__.SomeOtherClass object at 0x7f3e08d83bb0>
<__main__.SomeOtherClass object at 0x7f3e08d83bb0>
<__main__.SomeOtherClass object at 0x7f3e08d83bb0>
...

说实话,我真的不知道发生这种情况的原因,所以其他人可以帮助你更多。正如用户 Booboo 所说,您看到这种情况是因为 Linux 使用 fork 来启动一个新进程。我也确实在 Linux 机器上运行过它。如果使用 Windows,内存引用会有所不同。

【讨论】:

  • 进程是并行的。 GIL 不跨越进程,在fork 期间不涉及暂停或酸洗。
  • @AKX 不,不是在分叉期间,而是在进程更改期间。 afaik,multiprocessing 使用pickle 来保存进程中对象的状态,所以我认为被解封的实例可能最终与其他进程中的实例位于相同的内存地址。然而,multiprocessing 确实避开了 GIL
  • 分叉“过程的变化”。多处理仅在您显式跨进程发送对象时序列化(例如泡菜)对象,例如有队列。
  • 好的,我明白你在说什么,这是一个很好的解释。只有一件事我们都可能会误解:'分叉是“过程的改变”'。我可能没有正确地表达自己,当我说“流程的变化”时,我不是在谈论创建新流程,我已经知道这就是分叉,而是在谈论上下文切换。那里不涉及分叉。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-28
  • 1970-01-01
  • 2020-11-29
  • 2018-10-27
  • 2022-06-15
相关资源
最近更新 更多