Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is it thread-safe to perform lazy initialization in python?

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?

like image 241
canadadry Avatar asked Feb 27 '12 03:02

canadadry


People also ask

Does it matter which thread initializes a lazy<T> object?

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.

What is thread safe initialization in Java?

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.

Why do we use lazy initialization?

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.

What is lazythreadsafetymode in Lazy<T> Constructors?

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.


2 Answers

  1. No, no locks are added automatically.
  2. That's why this code is not thread-safe.
  3. If it seems to work in a multi-threaded program without problems, it's probably due to the Global Interpreter Lock, which makes the hazard less likely to occur.
like image 120
Niklas B. Avatar answered Oct 17 '22 09:10

Niklas B.


This code is not thread-safe.

Determining thread safety

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-safe version of LazyProperty

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

Canonical thread-safe initialization

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

Something completely different from metaclasses solution

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.

like image 45
thodnev Avatar answered Oct 17 '22 08:10

thodnev