我刚刚阅读了这篇关于懒惰地初始化对象属性的配方的blog post。 我是一个正在恢复的java程序员,如果这段代码被翻译成java,它将被视为竞争条件(双重检查锁定)。为什么它在python中工作?我知道python中有一个线程模块。解释器是否秘密添加了锁以使其具有线程安全性?
规范的线程安全初始化在Python中看起来如何?
答案 0 :(得分:7)
答案 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等)
所以,一般建议,如果你需要线程安全,请使用同步机制:锁,队列,信号量等。
您提到的描述符的线程安全性可以像这样:
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__]