I just read this blog post about a recipe to lazily initialize an object property. I am a recovering java programmer and if this code was translated into java, it would be considered a race condition (double check locking). Why does it work in python ? I know there is a threading module in python. Are locks added surreptitiously by the interpreter to make this thread-safe?
How does canonical thread-safe initialisation look in Python?
In multi-threaded scenarios, the first thread to access the Value property of a thread-safe Lazy<T> object initializes it for all subsequent accesses on all threads, and all threads share the same data. Therefore, it does not matter which thread initializes the object, and race conditions are benign.
Thread-Safe Initialization. By default, Lazy<T> objects are thread-safe. That is, if the constructor does not specify the kind of thread safety, the Lazy<T> objects it creates are thread-safe.
Lazy initialization is primarily used to improve performance, avoid wasteful computation, and reduce program memory requirements. These are the most common scenarios: When you have an object that is expensive to create, and the program might not use it.
Some Lazy<T> constructors have a LazyThreadSafetyMode parameter named mode. These constructors provide an additional thread safety mode. The following table shows how the thread safety of a Lazy<T> object is affected by constructor parameters that specify thread safety.
This code is not thread-safe.
You could check thread-safety by stepping through the bytecode, like:
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
However, I should warn, that bytecode could change over time and thread-safety could depend on python you use (cpython, jython, ironpython etc)
So, general recommendation, if you ever need thread-safety, use synchronization mechanisms: Locks, Queues, Semaphores, etc.
Thread-safety for descriptor you've mentioned, could be brought like this:
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__]
For a canonical thread-safe initialization, you need to code a metaclass, which acquires lock at creation time, and releases after the instance has been created:
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)
'''
And finally, if you need simpler solution, just create common init lock and create instances using it:
from threading import Lock
MyCls._inst_lock = Lock() # monkey patching | or subclass if hate it
...
with MyCls._inst_lock:
myinst = MyCls()
However, it's easy to forget which may bring a very interesting debugging times. Also possible to code a class decorator, but in my opinion, it would be no better, than metaclass solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With