Я хочу проверить поведение одноэлементного класса при использовании в многопроцессорной среде, потому что мне было доведено до сведения, что он не работает должным образом. Кажется, что один и тот же объект используется в двух разных процессах.
from threading import Lock
class SingletonMeta(type):
_instances = {}
_lock = Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
from singleton import SingletonMeta
from multiprocessing import Pool
class Test(metaclass=SingletonMeta):
def __init__(self, value):
self.value = value
def do(value):
t = Test(value)
return t.value, id(t)
if __name__ == "__main__":
with Pool(2) as pool:
values = [1, 2]
results = pool.map(do, values)
results.sort()
print("results: ", results)
from singleton import SingletonMeta
import pytest
from multiprocessing import Pool
class Singleton(metaclass=SingletonMeta):
def __init__(self, value):
self.value = value
def do(value):
t = Singleton(value=value)
return t.value, id(t)
def test_multiprocess():
# given
value1 = 1
value2 = 2
# when
with Pool(2) as pool:
results = pool.map(do, [value1, value2])
results.sort()
print("results", results)
# then
assert results[0][0] == value1
assert results[1][0] == value2
$ python manual_test.py
results: [(1, 2076407268784), (1, 2076407268784)]
Как видите, value
и идентификатор объекта одинаковы для обоих процессов.
примечание: усечено для удаления шума
$ pytest -rA -v -p no:faulthandler
results [(1, 2356600174768), (2, 2732965816496)]
Как видите, и значения, и идентификатор объекта разные.
Учитывая, что эти две программы имеют почти одинаковый код, я ожидал одинакового поведения для обеих:
Однако это происходит только при вызове manual_test.py, а не с помощью утилиты pytest. Моя конечная цель - заставить мой класс работать в режиме многопроцессорности и протестировать его в моей библиотеке, поэтому я хотел бы знать:
Какую ОС вы используете? Вы не указали, какой метод запуска использовать, поэтому выбирается метод по умолчанию для вашей ОС. На первый взгляд кажется, что используемый метод запуска может иметь отношение к объяснению поведения, которое вы видите.
@chepner Результат одинаков в обоих случаях.
@ Брайан Windows 10 64-разрядная версия, Python 3.8.3, pytest 6.1.2. Что вы подразумеваете под "методом запуска".
@ Itération122442 См. многопроцессорность — контексты и методы запуска
Ручной тест проходит из-за проблемы со временем (точнее, из-за нехватки потраченного времени/ресурсов); хотя я не разбираюсь в том, как python решает выделить объединенные процессы для заданных задач, это можно довольно легко продемонстрировать, проверив идентификатор процесса:
def do(value):
t = Test(value)
return t.value, id(t), os.getpid()
приведет к
results: [(1, 4311792512, 24184), (1, 4311792512, 24184)]
Которые такие же, как вы уже заметили. Однако;
добавление некоторых вычислений или времени к этой функции изменит результаты:
def do(value):
t = Test(value)
time.sleep(0.1)
return t.value, id(t), os.getpid()
->
results: [(1, 4321196928, 24285), (2, 4338334592, 24286)]
или
def do(value):
t = Test(value)
for x in range(100000):
pass
return t.value, id(t), os.getpid()
->
results: [(1, 4387257264, 24358), (2, 4338498480, 24359)]
pytest может добавить достаточно накладных расходов, чтобы вызвать разницу.
Основной проблемой здесь является разница между потоками и процессами; в то время как threading.Lock
хорошо работает для потоков, блокировка потока не влияет на другой процесс. С другой стороны, хотя вы можете переключиться на использование multiprocessing.Lock
для синглтона, это может только предотвратить его одновременное создание и не гарантирует один единственный класс для всех процессов. Это связано с тем, что даже после создания класса словарь cls._instances
не используется/синхронизируется между всеми процессами.
Существуют различные механизмы синхронизации, которые могут обеспечить такую координацию, но наилучший вариант зависит от конкретного варианта использования — что должно быть достигнуто, а что должно быть предотвращено.
Хороший улов, спасибо. Итак, что происходит, так это то, что процесс X используется дважды, а состояние синглтона не «очищается» между каждым набором инструкций, верно?
Что ж, думаю, вы можете так сказать, но мне кажется, что это будет неуклюжий взгляд на это. Я имею в виду, что цель синглтона в первую очередь состоит в том, чтобы быть уникальным, поэтому «очистка» его состояния для создания другого как бы побеждает цель, не так ли?
Да, в самом деле. Однако мы столкнулись с очень специфической проблемой, когда нам время от времени требуется различная конфигурация класса, наследующего синглтон. Но это другая история.
Я чувствую, что нанес достаточно "ущерба" для одного дня, но опять же - я мог бы быть в ударе здесь ;). Python довольно либерален, в отличие от некоторых других языков; хотя я видел несколько случаев, когда синглтоны полезны, python может не одобрять, но активно не мешает кому-либо возиться с частными методами. Рассматривали ли вы вместо этого использование метода __new__
класса? Я обнаружил, что такой подход может быть более удобным для чтения и сопровождения, чем метаклассы, и значительно упрощает наследование. Может не подойдет, просто мысль.
Тьфу, я бы вообще не стал с этим связываться — просто один раз инициализируйте свои переменные состояния и передайте их процессам как config. Не полагайтесь на синглтоны вообще при многопроцессорной обработке.
@micromoses Ты ничего не повреждаешь. Я беру все, что могу. Я посмотрю на это, принимая во внимание комментарий Дельгадо;)
Что произойдет, если вы сделаете свои тесты согласованными (либо используйте
value1 = 1; value2 = 2
в обоих, либо используйтеvalues = [1, 2]
в обоих)?