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

can*_*dry 7 python concurrency

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

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

Nik*_* B. 7

  1. 不,没有自动添加锁.
  2. 这就是为什么这段代码不是线程安全的.
  3. 如果它似乎在没有问题的多线程程序中工作,那可能是由于全局解释器锁定,这使得危险不太可能发生.


tho*_*nev 5

此代码不是线程安全的。

确定线程安全

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

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
Run Code Online (Sandbox Code Playgroud)

但是,我应该警告,字节码可能会随时间而变化,线程安全性可能取决于您使用的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__]
Run Code Online (Sandbox Code Playgroud)

规范线程安全的初始化

对于规范的线程安全初始化,您需要编写一个元类,该元类在创建时会获取锁,并在实例创建后释放:

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)
'''
Run Code Online (Sandbox Code Playgroud)

与元类解决方案完全不同

最后,如果您需要更简单的解决方案,只需创建通用的初始化锁并使用它来创建实例:

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

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