Singleton has same address in multiple processes, but behaves like separate objects

Somebody might be able to explain this with greater detail but I believe the problem -- and there is a problem which I will explain in a moment -- is that you have to first ask how are the arguments being passed from your main process to your new subprocesses. Well, it would seem that would depend on what platform you are running under, which you neglected to specify but I can infer is one that uses fork to create new processes. I believe that because your platform supports forking and therefore inherits the address space (read/only, copy on write) from the main process, no special mechanism such as pickle is required to serialize/deserialize the arguments and that is why the addresses are the same.

But here's the rub: Under Windows, which does not support fork and will rely on pickle for propagating Process arguments, the Message.__new__ method will be re-executed in each of the newly created processes. Not only will the singletons have their own unique addresses (unless a miracle occurs), they will be allocating their own multiprocessing.Pipe instances and then, of course, the application will not work at all as demonstrated below. You would be better off just passing multiprocessing.Connection objects to your subprocesses.

from multiprocessing import Process, Pipe
import logging
import time

class MessagePipe:
    """This implements a multiprocessing message pipe as a singleton and is intended for passing event information
    such as log messaged and exceptions between processes.
    """

    _instance = None
    _parent_connection = None
    _child_connection = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            print('__new__ is executing')
            cls._instance = super(MessagePipe, cls).__new__(cls, *args, **kwargs)
            cls._parent_connection, cls._child_connection = Pipe()
        return cls._instance

    @property
    def child_connection(self):
        return self._child_connection

    @property
    def parent_connection(self):
        return self._parent_connection

    def send(self, text="", exception=None, level=logging.INFO):
        message = {
            "text": text,
            "exception": exception,
            "level": level
        }
        self.child_connection.send(message)

    def recv(self):
        return self.parent_connection.recv()

def keep_receiving(pipe):
    for i in range(3):
        print("from rec_proc: ", pipe)
        print(pipe.recv())

def keep_sending(pipe):
    for i in range(3):
        print("from send_proc:", pipe)
        pipe.send(text=f"test {i + 1}")
        time.sleep(1)

def v2():
    send_proc = Process(target=keep_sending, args=(MessagePipe(), ))
    send_proc.start()

    rec_proc = Process(target=keep_receiving, args=(MessagePipe(), ))
    rec_proc.start()

    send_proc.join()
    rec_proc.join()


if __name__ == '__main__':
    v2()

Prints:

__new__ is executing
__new__ is executing
from send_proc: <__mp_main__.MessagePipe object at 0x00000283480705E0>
from rec_proc:  <__mp_main__.MessagePipe object at 0x00000249497A05E0>
from send_proc: <__mp_main__.MessagePipe object at 0x00000283480705E0>
from send_proc: <__mp_main__.MessagePipe object at 0x00000283480705E0>

rec_proc hangs because it is reading from a connection that nobody is writing to.

The problem can be somewhat mitigated by specifying pickle __setstate__ and __getstate__ methods for your MessagePipe class. This will not stop your __new__ method from being called and creating a new Pipe instances in the subprocesses but it will replace them with the serialized arguments that were passed.

from multiprocessing import Process, Pipe
import logging
import time

class MessagePipe:
    """This implements a multiprocessing message pipe as a singleton and is intended for passing event information
    such as log messaged and exceptions between processes.
    """

    _instance = None
    _parent_connection = None
    _child_connection = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            print('__new__ is executing')
            cls._instance = super(MessagePipe, cls).__new__(cls, *args, **kwargs)
            cls._parent_connection, cls._child_connection = Pipe()
        return cls._instance

    @property
    def child_connection(self):
        return self._child_connection

    @property
    def parent_connection(self):
        return self._parent_connection

    def send(self, text="", exception=None, level=logging.INFO):
        message = {
            "text": text,
            "exception": exception,
            "level": level
        }
        self.child_connection.send(message)

    def recv(self):
        return self.parent_connection.recv()

    def __getstate__(self):
        return (self._parent_connection, self._child_connection)

    def __setstate__(self, state):
        self._parent_connection, self._child_connection = state

def keep_receiving(pipe):
    for i in range(3):
        print("from rec_proc: ", pipe)
        print(pipe.recv())

def keep_sending(pipe):
    for i in range(3):
        print("from send_proc:", pipe)
        pipe.send(text=f"test {i + 1}")
        time.sleep(1)

def v2():
    send_proc = Process(target=keep_sending, args=(MessagePipe(), ))
    send_proc.start()

    rec_proc = Process(target=keep_receiving, args=(MessagePipe(), ))
    rec_proc.start()

    send_proc.join()
    rec_proc.join()


if __name__ == '__main__':
    v2()

Prints:

__new__ is executing
__new__ is executing
__new__ is executing
from send_proc: <__mp_main__.MessagePipe object at 0x00000220220F15E0>
from rec_proc:  <__mp_main__.MessagePipe object at 0x000001BB2F5C15E0>
{'text': 'test 1', 'exception': None, 'level': 20}
from rec_proc:  <__mp_main__.MessagePipe object at 0x000001BB2F5C15E0>
from send_proc: <__mp_main__.MessagePipe object at 0x00000220220F15E0>
{'text': 'test 2', 'exception': None, 'level': 20}
from rec_proc:  <__mp_main__.MessagePipe object at 0x000001BB2F5C15E0>
from send_proc: <__mp_main__.MessagePipe object at 0x00000220220F15E0>
{'text': 'test 3', 'exception': None, 'level': 20}

Another, more classic implementation of the singleton pattern that does not use method __new__ avoids creating new Pipe instances and their corresponding connections only to have them discarded by the piclke __setstate__ method when it restores the original serialized connections:

from multiprocessing import Process, Pipe
import logging
import time

class MessagePipe:
    """This implements a multiprocessing message pipe as a singleton and is intended for passing event information
    such as log messaged and exceptions between processes.
    """

    _instance = None
    _parent_connection = None
    _child_connection = None

    def __init__(self):
        raise RuntimeError('Call getInstance() instead')

    @classmethod
    def getInstance(cls):
        if cls._instance is None:
            print('Creating new instance.')
            cls._instance = cls.__new__(cls)
            cls._parent_connection, cls._child_connection = Pipe()
        return cls._instance

    @property
    def child_connection(self):
        return self._child_connection

    @property
    def parent_connection(self):
        return self._parent_connection

    def send(self, text="", exception=None, level=logging.INFO):
        message = {
            "text": text,
            "exception": exception,
            "level": level
        }
        self.child_connection.send(message)

    def recv(self):
        return self.parent_connection.recv()

    def __getstate__(self):
        return (self._parent_connection, self._child_connection)

    def __setstate__(self, state):
        self._parent_connection, self._child_connection = state

def keep_receiving(pipe):
    for i in range(3):
        print("from rec_proc: ", pipe)
        print(pipe.recv())

def keep_sending(pipe):
    for i in range(3):
        print("from send_proc:", pipe)
        pipe.send(text=f"test {i + 1}")
        time.sleep(1)

def v2():
    send_proc = Process(target=keep_sending, args=(MessagePipe.getInstance(), ))
    send_proc.start()

    rec_proc = Process(target=keep_receiving, args=(MessagePipe.getInstance(), ))
    rec_proc.start()

    send_proc.join()
    rec_proc.join()


if __name__ == '__main__':
    v2()

Prints:

Creating new instance.
from send_proc: <__mp_main__.MessagePipe object at 0x0000023459610520>
from rec_proc:  <__mp_main__.MessagePipe object at 0x0000022F2D8A1520>
{'text': 'test 1', 'exception': None, 'level': 20}
from rec_proc:  <__mp_main__.MessagePipe object at 0x0000022F2D8A1520>
from send_proc: <__mp_main__.MessagePipe object at 0x0000023459610520>
{'text': 'test 2', 'exception': None, 'level': 20}
from rec_proc:  <__mp_main__.MessagePipe object at 0x0000022F2D8A1520>
from send_proc: <__mp_main__.MessagePipe object at 0x0000023459610520>
{'text': 'test 3', 'exception': None, 'level': 20}

On any modern OS, these are virtual memory adresses. These addresses are local to each process' memory space. They almost never correspond to the same physical memory cell 0x7f805e769610. You need to google "virtual memory"


Modern OS use virtual memory. That means that physical memory pages are mapped into the process address space. Because of that, the loader can give to multiple instances of the same program the same virtual addresses. So in every process the singleton will have the same virtual address, but as those virtual addresses are mapped to different physical pages, they will point to distinct objects.