Почему поточно-безопасно выполнять ленивую инициализацию в python? - PullRequest
7 голосов
/ 27 февраля 2012

Я только что прочитал сообщение в блоге о рецепте ленивой инициализации свойства объекта.Я программист на восстановление Java, и если этот код был переведен в Java, это будет считаться условием гонки (двойная проверка блокировки).Почему это работает в Python?Я знаю, что в Python есть модуль потоков.Добавлены ли тайком блокировки блокировки интерпретатором, чтобы сделать этот потокобезопасным?

Как выглядит каноническая потокобезопасная инициализация в Python?

Ответы [ 3 ]

7 голосов
/ 27 февраля 2012
  1. Нет, блокировки не добавляются автоматически.
  2. Вот почему этот код не поточно-ориентированный.
  3. Если кажется, что он работает в многопоточной программе без проблем, это, вероятно, связано с Global Interpreter Lock , которая снижает вероятность возникновения опасности.
4 голосов
/ 30 августа 2016

Этот код не является потокобезопасным.

Определение безопасности потока

Вы можете проверить безопасность потоков, пройдя байт-код, например:

from dis import dis

dis('a = [] \n'
    'a.append(5)')
# Here you could see that it's thread safe
##  1           0 BUILD_LIST               0
##              3 STORE_NAME               0 (a)
##
##  2           6 LOAD_NAME                0 (a)
##              9 LOAD_ATTR                1 (append)
##             12 LOAD_CONST               0 (5)
##             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
##             18 POP_TOP
##             19 LOAD_CONST               1 (None)
##             22 RETURN_VALUE

dis('a = [] \n'
    'a += 5')
# And this one isn't (possible gap between 15 and 16)
##  1           0 BUILD_LIST               0
##              3 STORE_NAME               0 (a)
##
##  2           6 LOAD_NAME                0 (a)
##              9 LOAD_CONST               0 (5)
##             12 BUILD_LIST               1
##             15 BINARY_ADD
##             16 STORE_NAME               0 (a)
##             19 LOAD_CONST               1 (None)
##             22 RETURN_VALUE

Однако я должен предупредить, что байт-код может со временем меняться, а безопасность потоков может зависеть от используемого вами Python (cpython, jython, ironpython и т. Д.)

Итак, общая рекомендация: если вам когда-либо понадобится безопасность потоков, используйте механизмы синхронизации: блокировки, очереди, семафоры и т. Д.

Поточно-ориентированная версия LazyProperty

Потоковая безопасность для дескриптора, который вы упомянули, может быть приведена так:

from threading import Lock

class LazyProperty(object):

    def __init__(self, func):
        self._func = func
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__
        self._lock = Lock()

    def __get__(self, obj, klass=None):
        if obj is None: return None
        # __get__ may be called concurrently
        with self.lock:
            # another thread may have computed property value
            # while this thread was in __get__
            # line below added, thx @qarma for correction
            if self.__name__ not in obj.__dict__: 
                # none computed `_func` yet, do so (under lock) and set attribute
                obj.__dict__[self.__name__] = self._func(obj)
        # by now, attribute is guaranteed to be set,
        # either by this thread or another
        return obj.__dict__[self.__name__]

Каноническая поточно-ориентированная инициализация

Для канонической поточно-ориентированной инициализации вам необходимо кодировать метакласс, который получает блокировку во время создания и освобождает после создания экземпляра:

from threading import Lock

class ThreadSafeInitMeta(type):
    def __new__(metacls, name, bases, namespace, **kwds):
        # here we add lock to !!class!! (not instance of it)
        # class could refer to its lock as: self.__safe_init_lock
        # see namespace mangling for details
        namespace['_{}__safe_init_lock'.format(name)] = Lock()
        return super().__new__(metacls, name, bases, namespace, **kwds)

    def __call__(cls, *args, **kwargs):
        lock = getattr(cls, '_{}__safe_init_lock'.format(cls.__name__))
        with lock:
            retval = super().__call__(*args, **kwargs)
        return retval


class ThreadSafeInit(metaclass=ThreadSafeInitMeta):
    pass

######### Use as follows #########
# class MyCls(..., ThreadSafeInit):
#     def __init__(self, ...):
#         ...
##################################

'''
class Tst(ThreadSafeInit):
    def __init__(self, val):
        print(val, self.__safe_init_lock)
'''

Что-то совершенно отличное от решения метаклассов

И, наконец, если вам нужно более простое решение, просто создайте общую блокировку инициализации и создайте ее с помощью экземпляров:

from threading import Lock
MyCls._inst_lock = Lock()  # monkey patching | or subclass if hate it
...
with MyCls._inst_lock:
   myinst = MyCls()

Однако легко забыть, что может привести к очень интересным временам отладки. Также возможно закодировать декоратор класса, но на мой взгляд, это будет не лучше, чем решение метакласса.

1 голос
/ 31 августа 2016

Чтобы расширить ответ @ thodnev, вот как защитить lazy свойство инициализация:

class LazyProperty(object):

    def __init__(self, func):
        self._func = func
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__
        self.lock = threading.Lock()

    def __get__(self, obj, klass=None):
        if obj is None: return None
        # __get__ may be called concurrently
        with self.lock:
            # another thread may have computed property value
            # while this thread was in __get__
            if self.__name__ not in obj.__dict__:
                # none computed `_func` yet, do so (under lock) and set attribute
                obj.__dict__[self.__name__] = self._func(obj)
        # by now, attribute is guaranteed to be set,
        # either by this thread or another
        return obj.__dict__[self.__name__]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...