【问题标题】:Why is it thread-safe to perform lazy initialization in python?为什么在 python 中执行延迟初始化是线程安全的?
【发布时间】:2012-03-16 14:30:30
【问题描述】:

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

规范的线程安全初始化在 Python 中的表现如何?

【问题讨论】:

  • @NiklasB。你应该这样回答。
  • 谁告诉你它有效的?谈到线程,你永远不应该相信一个天真的测试...... ;)
  • @qarma:接受的答案到底缺少什么?应该解决哪些问题?

标签: python concurrency


【解决方案1】:
  1. 不,不会自动添加锁。
  2. 这就是为什么这段代码不是线程安全的。
  3. 如果它似乎在多线程程序中运行没有问题,那可能是由于Global Interpreter Lock 导致的,它降低了发生危险的可能性。

【讨论】:

  • 由于 GIL,它可以在 Cython 中运行。但是这段代码会在 Jython 中中断。好的,明白了。
  • @BonAmi:CPython,不是 Cython。 Cython 完全是另一回事。但这是一般的想法;它应该在 CPython 中工作,但在任何情况下依赖它都是一个坏主意。
  • @NiklasB.:您的新措辞改进了很多很多,感谢您的编辑。 :-) 我发现这个是因为这个问题有点奇怪。
  • @MartijnPieters 哦,我没有注意到有赏金。感谢您指出我的这个(非常老的)答案的缺点
【解决方案2】:

此代码不是线程安全的。

确定线程安全

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

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

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

【讨论】:

  • 元类方法对派生类也能正常工作吗?我的意思是,如果有A->B->meta__new__ 会为name 获得“A”或“B”吗?
  • @qarma 是的,它会的。正如预期的那样,每个创建的子类都会有自己的__safe_init_lock 版本
  • 酷。如果将其应用于原始问题,我认为这篇文章仍然缺少一步。考虑链接博客文章中的LazyProperty,AFAIK 无法确保__get__ 只被调用一次,因此诀窍是保护__get__ 方法内的计算和属性分配,对吧?
  • @qarma 实际上它是 python 继承的一个棘手部分:当你使用 .dot 限定时,它会启动一个新的搜索(实例 -> 类 -> 类的父级 -> 等等。)所以只要当找到第一个出现时,停止进一步的搜索过程。当您在实例中设置相同的属性然后尝试获取它时,您将获得与实例中完全相同的属性。您可以尝试将打印跟踪添加到描述符,我保证它只会打印一次
  • @qarma 但你是对的,__get__ 可能被并发线程大致同时调用,这就是我们使用锁来确保它是线程安全的原因
【解决方案3】:

为了扩展@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__]

【讨论】:

  • 哦,我最初错过了,现在添加到回答中
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-17
  • 1970-01-01
  • 2015-07-27
  • 1970-01-01
  • 2014-07-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多