Я полагал, что создание экземпляра синглтона в разных процессах приведет к разным объектам, так что синглтон будет локальным только для своего собственного процесса. Чтобы проверить это, я написал тест:

def v1():
    def keep_receiving():
        while True:
            print("from rec_proc: ", MessagePipe())
            print(MessagePipe().recv())

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

    send_proc = Process(target=keep_sending)
    send_proc.start()

    rec_proc = Process(target=keep_receiving)
    rec_proc.start()

    send_proc.join()

Я ожидал, что два процесса будут распечатывать объекты по разным адресам памяти. Но это не тот случай:

Выход:

from send_proc: <image_service.service.utils.message_pipe.MessagePipe object at 0x7f805e769610>
from rec_proc:  <image_service.service.utils.message_pipe.MessagePipe object at 0x7f805e769610>
from send_proc: <image_service.service.utils.message_pipe.MessagePipe object at 0x7f805e769610>
from send_proc: <image_service.service.utils.message_pipe.MessagePipe object at 0x7f805e769610>

Как видите, адреса совпадают.

Вопрос: Почему адреса одинаковые, если это ДВА разных экземпляра, каждый в своем процессе?

Это должны быть два разных экземпляра, иначе строка print(MessagePipe().recv()) должна вывести три полученных сообщения.

Когда я изменяю тест таким образом, я получаю ожидаемое поведение:

def v2():
    def keep_receiving(pipe):
        while True:
            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)

    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()
from send_proc: <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>
from rec_proc:  <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>
{'text': 'test 1', 'exception': None, 'level': 20}
from rec_proc:  <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>
from send_proc: <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>
{'text': 'test 2', 'exception': None, 'level': 20}
from rec_proc:  <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>
from send_proc: <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>
{'text': 'test 3', 'exception': None, 'level': 20}
from rec_proc:  <image_service.service.utils.message_pipe.MessagePipe object at 0x7f51c2dff670>

Класс MessagePipe реализован следующим образом, но я думаю, что реализация не является центральной в вопросе.

import logging
from multiprocessing import Pipe


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:
            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()
0
lo tolmencre 18 Янв 2022 в 19:13

3 ответа

В любой современной ОС это адреса виртуальной памяти. Эти адреса являются локальными для пространства памяти каждого процесса. Они почти никогда не соответствуют одной и той же ячейке физической памяти 0x7f805e769610. Вам нужно погуглить "виртуальную память"

0
Expurple 18 Янв 2022 в 19:27
Это не совсем правильно. Смотрите мой комментарий к ответу Сержа Бальесты.
 – 
Booboo
18 Янв 2022 в 20:42

Современные ОС используют виртуальную память. Это означает, что страницы физической памяти отображаются в адресное пространство процесса. Из-за этого загрузчик может давать нескольким экземплярам одной и той же программы одни и те же виртуальные адреса. Таким образом, в каждом процессе синглтон будет иметь один и тот же виртуальный адрес, но поскольку эти виртуальные адреса сопоставляются с разными физическими страницами, они будут указывать на разные объекты.

0
Serge Ballesta 18 Янв 2022 в 19:50
Это не совсем так. Только потому, что os fork использовался для создания нового подпроцесса, адрес тот же, т. е. потому что pickle не нужно было использовать для сериализации/десериализации аргументов. Под Wndows адрес будет другим и, по сути, программа перестанет работать корректно, потому что метод __new__ будет повторно выполняться заново в каждом новом подпроцессе, создающем новые экземпляры Pipe.
 – 
Booboo
18 Янв 2022 в 20:41

Кто-то мог бы объяснить это более подробно, но я считаю, что проблема — и есть проблема, которую я объясню через мгновение — заключается в том, что вы должны сначала спросить, как аргументы передаются из вашего основного процесса в ваш. новые подпроцессы. Что ж, похоже, это будет зависеть от того, на какой платформе вы работаете, которую вы не указали, но я могу сделать вывод, что это та, которая использует fork для создания новых процессов. Я полагаю, что, поскольку ваша платформа поддерживает разветвление и, следовательно, наследует адресное пространство (только чтение/только, копирование при записи) от основного процесса, для сериализации/десериализации аргументов и не требуется никакого специального механизма, такого как pickle. поэтому адреса совпадают.

Но вот загвоздка: в Windows, которая не поддерживает fork и будет полагаться на pickle для распространения Process аргументов, метод Message.__new__ будет выполняться повторно. в каждом из вновь созданных процессов. Мало того, что синглтоны будут иметь свои собственные уникальные адреса (если только не произойдет чудо), они будут выделять свои собственные multiprocessing.Pipe экземпляры, и тогда, конечно же, приложение вообще не будет работать, как показано ниже. Вам было бы лучше просто передать объекты multiprocessing.Connection своим подпроцессам.

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()

Печать:

__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 зависает, потому что он читает из соединения, в которое никто не записывает.

Эту проблему можно несколько смягчить, указав методы pickle __setstate__ и __getstate__ для вашего класса MessagePipe. Это не остановит вызов вашего метода __new__ и создание новых экземпляров Pipe в подпроцессах, но заменит их переданными сериализованными аргументами.

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()

Печать:

__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}

Я действительно не уверен, почему вы потрудились создать класс MessagePipe как синглтон. Я бы просто сделал:

from multiprocessing import Process, Pipe
import logging
import time

class MessagePipe:
    def __init__(self):
        self._parent_connection, self._child_connection = Pipe()

    @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():
    message_pipe = MessagePipe()

    send_proc = Process(target=keep_sending, args=(message_pipe, ))
    send_proc.start()

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

    send_proc.join()
    rec_proc.join()


if __name__ == '__main__':
    v2()

Печать:

from send_proc: <__mp_main__.MessagePipe object at 0x0000025D693A0610>
from rec_proc:  <__mp_main__.MessagePipe object at 0x000001585C0A0610>
{'text': 'test 1', 'exception': None, 'level': 20}
from rec_proc:  <__mp_main__.MessagePipe object at 0x000001585C0A0610>
from send_proc: <__mp_main__.MessagePipe object at 0x0000025D693A0610>
{'text': 'test 2', 'exception': None, 'level': 20}
from rec_proc:  <__mp_main__.MessagePipe object at 0x000001585C0A0610>
from send_proc: <__mp_main__.MessagePipe object at 0x0000025D693A0610>
{'text': 'test 3', 'exception': None, 'level': 20}
0
Booboo 18 Янв 2022 в 20:54