为什么在python中执行延迟初始化是线程安全的?

时间:2012-02-27 03:10:22

标签: python concurrency

我刚刚阅读了这篇关于懒惰地初始化对象属性的配方的blog post。 我是一个正在恢复的java程序员,如果这段代码被翻译成java,它将被视为竞争条件(双重检查锁定)。为什么它在python中工作?我知道python中有一个线程模块。解释器是否秘密添加了锁以使其具有线程安全性?

规范的线程安全初始化在Python中看起来如何?

3 个答案:

答案 0 :(得分:7)

  1. 不,不会自动添加锁。
  2. 这就是为什么这段代码线程安全。
  3. 如果它似乎在没有问题的多线程程序中工作,可能是由于Global Interpreter Lock,这使得危险发生的可能性降低。

答案 1 :(得分:4)

此代码不是线程安全的。

确定线程安全性

您可以通过逐步执行字节码来检查线程安全性,例如:

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

与元类解决方案完全不同的东西

最后,如果您需要更简单的解决方案,只需创建公共init锁并使用它创建实例:

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

然而,很容易忘记哪些可能带来非常有趣的调试时间。 也可以编写类装饰器,但在我看来,它不会比元类解决方案更好。

答案 2 :(得分:1)

要扩展@ thodnev的答案,这里是如何保护懒惰的属性初始化:

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