Skip to content

Latest commit

 

History

History
369 lines (277 loc) · 10.6 KB

File metadata and controls

369 lines (277 loc) · 10.6 KB

Thread-Safe Singleton Pattern in Python

Slide 1: Introduction to the Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This pattern is useful when exactly one object is needed to coordinate actions across the system.

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

# Usage
s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # Output: True

Slide 2: The Need for Thread Safety

In multi-threaded environments, the basic Singleton implementation can fail if multiple threads attempt to create the instance simultaneously. This can result in multiple instances being created, violating the Singleton principle.

import threading

def create_singleton():
    s = Singleton()
    print(f"Instance created by thread {threading.current_thread().name}")

# This might create multiple instances in a multi-threaded environment
threads = [threading.Thread(target=create_singleton) for _ in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

Slide 3: Double-Checked Locking Pattern

The double-checked locking pattern is an attempt to reduce the overhead of acquiring a lock by first testing the locking criterion without actually acquiring the lock. However, this pattern can be problematic in some programming languages due to memory model issues.

import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

Slide 4: Thread-Safe Singleton Using Metaclass

A metaclass can be used to implement a thread-safe Singleton pattern. This approach ensures that the Singleton behavior is inherent to the class itself, rather than relying on the implementation of new.

import threading

class SingletonMeta(type):
    _instances = {}
    _lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        with cls._lock:
            if cls not in cls._instances:
                instance = super().__call__(*args, **kwargs)
                cls._instances[cls] = instance
        return cls._instances[cls]

class Singleton(metaclass=SingletonMeta):
    pass

Slide 5: Lazy Initialization in Thread-Safe Singleton

Lazy initialization defers the creation of the Singleton instance until it's first accessed. This can be beneficial for resource management, especially when the Singleton is resource-intensive to create.

import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance.initialize()
        return cls._instance

    def initialize(self):
        # Perform any resource-intensive initialization here
        pass

Slide 6: Borg Pattern: A Singleton Variant

The Borg pattern (also known as the Monostate pattern) is an alternative to the Singleton. It allows multiple instances of a class to share the same state, achieving a similar effect to Singleton but with a different approach.

class Borg:
    _shared_state = {}

    def __init__(self):
        self.__dict__ = self._shared_state

class SingletonBorg(Borg):
    def __init__(self):
        super().__init__()
        if not hasattr(self, 'instance'):
            self.instance = 'Borg Singleton'

    def __str__(self):
        return self.instance

# Usage
b1 = SingletonBorg()
b2 = SingletonBorg()
print(b1 is b2)  # False
print(b1.instance, b2.instance)  # Same value

Slide 7: Thread-Safe Singleton Using Module-Level Variables

In Python, module-level variables are singleton by nature. This approach leverages Python's module import mechanism to create a thread-safe Singleton.

# In file singleton.py
import threading

class Singleton:
    def __init__(self):
        self.value = None

    def set_value(self, value):
        self.value = value

_instance = None
_lock = threading.Lock()

def get_instance():
    global _instance
    if _instance is None:
        with _lock:
            if _instance is None:
                _instance = Singleton()
    return _instance

# Usage in another file
from singleton import get_instance

instance1 = get_instance()
instance2 = get_instance()
print(instance1 is instance2)  # True

Slide 8: Singleton with Custom Exception

Adding custom exceptions to your Singleton implementation can help in debugging and maintaining the code. This example demonstrates how to raise an exception when attempting to create multiple instances.

class SingletonException(Exception):
    pass

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is not None:
            raise SingletonException("Singleton instance already exists")
        cls._instance = super().__new__(cls)
        return cls._instance

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

# Usage
s1 = Singleton.get_instance()
s2 = Singleton.get_instance()
print(s1 is s2)  # True
Singleton()  # Raises SingletonException

Slide 9: Singleton with init Method

When implementing a Singleton, it's important to consider how the init method behaves. This example shows how to ensure that initialization occurs only once, even if the Singleton is accessed multiple times.

import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()
    _initialized = False

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if not self._initialized:
            with self._lock:
                if not self._initialized:
                    self.some_attribute = "Initialized only once"
                    self._initialized = True

# Usage
s1 = Singleton()
s2 = Singleton()
print(s1.some_attribute)  # "Initialized only once"
print(s2.some_attribute)  # "Initialized only once"
print(s1 is s2)  # True

Slide 10: Singleton with Parameters

Sometimes, you might need to pass parameters when creating a Singleton instance. This example demonstrates how to implement a Singleton that accepts parameters during initialization.

import threading

class Singleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialized = False
        return cls._instance

    def __init__(self, value=None):
        if not self._initialized:
            self.value = value
            self._initialized = True

# Usage
s1 = Singleton("First")
s2 = Singleton("Second")
print(s1.value)  # "First"
print(s2.value)  # "First"
print(s1 is s2)  # True

Slide 11: Testing Thread-Safe Singleton

Testing is crucial to ensure that your Singleton implementation is truly thread-safe. This example demonstrates a simple test case using Python's threading module.

import threading
import unittest

class TestSingleton(unittest.TestCase):
    def test_thread_safety(self):
        instances = []

        def create_instance():
            instances.append(Singleton())

        threads = [threading.Thread(target=create_instance) for _ in range(10)]
        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()

        self.assertEqual(len(set(instances)), 1)

if __name__ == '__main__':
    unittest.main()

Slide 12: Singleton Pattern in Real-World Scenarios

Singletons are often used for managing shared resources or coordinating actions across a system. This example demonstrates a simple logger implementation using the Singleton pattern.

import threading

class Logger:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance.log_file = open("application.log", "w")
        return cls._instance

    def log(self, message):
        with self._lock:
            self.log_file.write(f"{message}\n")
            self.log_file.flush()

# Usage
logger1 = Logger()
logger2 = Logger()
logger1.log("Log message from logger1")
logger2.log("Log message from logger2")
# Both messages are written to the same file

Slide 13: Singleton vs Dependency Injection

While Singletons can be useful, they can also make code harder to test and maintain. Dependency Injection is an alternative that can provide similar benefits with more flexibility. This example compares the two approaches.

# Singleton approach
class DatabaseConnection:
    _instance = None

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def query(self, sql):
        # Execute query
        pass

# Dependency Injection approach
class DatabaseConnection:
    def query(self, sql):
        # Execute query
        pass

class Service:
    def __init__(self, db_connection):
        self.db = db_connection

    def do_something(self):
        self.db.query("SELECT * FROM table")

# Usage with DI
db = DatabaseConnection()
service = Service(db)
service.do_something()

Slide 14: Additional Resources

For further exploration of the Singleton pattern and its implementations in Python, consider the following resources:

  1. "Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma et al. - The original book introducing design patterns.
  2. "Python Design Patterns" by Chetan Giridhar - A comprehensive guide to design patterns in Python.
  3. ArXiv.org paper: "A Survey of Object Oriented Design Patterns" by Prashant Jamwal - An overview of various design patterns, including Singleton. Reference: arXiv:2004.09159 [cs.SE]
  4. Python official documentation on threading: https://docs.python.org/3/library/threading.html

These resources provide deeper insights into the Singleton pattern and its applications in software design.