Effortless Efficiency: Exploring the Easy Double-Checked Locking Pattern in Python

Photo by Nicholas Githiri: https://www.pexels.com/photo/close-up-photography-of-wet-padlock-1068349/

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:

  1. We need a lock, in our case an re-entrant RLock to secure the value in our locker.
  2. We also need to make sure the value is initialized, and we ultimately need the value
  3. The initializer just initializes the lock and sets the value to None . The initialized flag is set to false.
  4. The _init_value() method has a function, which has a Callable 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.
  5. 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:

  1. We create our locker
  2. And try and get the car
  3. 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:

  1. Make it more generic
  2. Make it possible to pass an initialization (preferable with some parameters) to the constructor.
  3. Write a real multi-threaded example

Leave a Reply

Your email address will not be published. Required fields are marked *