Introduction
Sometimes when locking data or objects it can be handy to reduce the overhead of acquiring a lock on such objects by first checking whether a lock is really necessary. This typically used in combination with lazy initialization in multi-threaded applications, sometimes as part of a Singleton pattern.
In Python we can use the Lock
and RLock
classes to achieve this.
Implementation in Python
In this small example we will implement an ExpensiveCar
which we will need to protect. Let’s start by importing the necessary classes:
from threading import RLock
Next we can implement the ExpensiveCar
class. In our world a car has just a name and a price. The price is represented as an integer.
class ExpensiveCar:
_name: str = None
_price: int = None
def __init__(self, name: str = "default", price: int = 100) -> None:
self._name = name
self._price = price
def __repr__(self) -> str:
return f"ExpensiveCar(name={self._name},price={self._price})"
We need to wrap this in a class which performs the doublechecked locking:
class DoubleCheckedCarLocker:
_lock: RLock = None
_initialized: bool = False
_value: ExpensiveCar | None = None
def __init__(self) -> None:
self._lock = RLock()
self._initialized = False
self._value = None
def _init_value(self, init: Callable[[], ExpensiveCar]) -> None:
self._value = init()
self._initialized = True
def get_or_init(self, init: Callable[[], ExpensiveCar]) -> ExpensiveCar:
if self._initialized:
return self._value
with self._lock:
if not self._initialized:
self._init_value(init)
return self._value
Some remarks:
- We need a lock, in our case an re-entrant
RLock
to secure the value in our locker. - We also need to make sure the value is initialized, and we ultimately need the value
- The initializer just initializes the lock and sets the value to
None
. The initialized flag is set to false. - The
_init_value()
method has a function, which has aCallable
as a parameter. This is to specify the initialization function for our object. In our case the function has no parameters and returns an expensive car. From the_init_value()
this is function to initialize the value of the locker. - The
get_or_init()
method is the heart of this pattern. First we see if we have an initialized value, if so, we return that, if not, we lock the locker, and return the value.
Testing time
Now we can test our simple setup:
if __name__ == "__main__":
locked_value = DoubleCheckedCarLocker()
value = locked_value.get_or_init(ExpensiveCar)
print(f"Value is {value}\n\n")
Line by line:
- We create our locker
- And try and get the car
- Finally we print it out
Conclusion
Implementing this pattern in Python was quite easy. Note that we have a fast and slow path, and that by using double checked locking we can improve performance by avoiding unnecessary locking.
There are a couple of possible enhancement, which I will probably write about in upcoming articles:
- Make it more generic
- Make it possible to pass an initialization (preferable with some parameters) to the constructor.
- Write a real multi-threaded example