|
|
|
|
|
|
|
|
|
import functools |
|
import threading |
|
|
|
|
|
class Singleton(object): |
|
"""A base class for a class of a singleton object. |
|
|
|
For any derived class T, the first invocation of T() will create the instance, |
|
and any future invocations of T() will return that instance. |
|
|
|
Concurrent invocations of T() from different threads are safe. |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_lock_lock = threading.RLock() |
|
_lock = None |
|
|
|
|
|
_instance = None |
|
|
|
_is_shared = None |
|
|
|
def __new__(cls, *args, **kwargs): |
|
|
|
|
|
|
|
|
|
assert not kwargs.get("shared", False) or (len(args) + len(kwargs)) == 0, ( |
|
"Cannot use constructor arguments when accessing a Singleton without " |
|
"specifying shared=False." |
|
) |
|
|
|
|
|
|
|
if not cls._instance: |
|
|
|
if cls._lock is None: |
|
with cls._lock_lock: |
|
if cls._lock is None: |
|
cls._lock = threading.RLock() |
|
|
|
|
|
if not cls._instance: |
|
with cls._lock: |
|
if not cls._instance: |
|
cls._instance = object.__new__(cls) |
|
|
|
|
|
|
|
|
|
cls._instance.__init__() |
|
cls.__init__ = lambda *args, **kwargs: None |
|
|
|
return cls._instance |
|
|
|
def __init__(self, *args, **kwargs): |
|
"""Initializes the singleton instance. Guaranteed to only be invoked once for |
|
any given type derived from Singleton. |
|
|
|
If shared=False, the caller is requesting a singleton instance for their own |
|
exclusive use. This is only allowed if the singleton has not been created yet; |
|
if so, it is created and marked as being in exclusive use. While it is marked |
|
as such, all attempts to obtain an existing instance of it immediately raise |
|
an exception. The singleton can eventually be promoted to shared use by calling |
|
share() on it. |
|
""" |
|
|
|
shared = kwargs.pop("shared", True) |
|
with self: |
|
if shared: |
|
assert ( |
|
type(self)._is_shared is not False |
|
), "Cannot access a non-shared Singleton." |
|
type(self)._is_shared = True |
|
else: |
|
assert type(self)._is_shared is None, "Singleton is already created." |
|
|
|
def __enter__(self): |
|
"""Lock this singleton to prevent concurrent access.""" |
|
type(self)._lock.acquire() |
|
return self |
|
|
|
def __exit__(self, exc_type, exc_value, exc_tb): |
|
"""Unlock this singleton to allow concurrent access.""" |
|
type(self)._lock.release() |
|
|
|
def share(self): |
|
"""Share this singleton, if it was originally created with shared=False.""" |
|
type(self)._is_shared = True |
|
|
|
|
|
class ThreadSafeSingleton(Singleton): |
|
"""A singleton that incorporates a lock for thread-safe access to its members. |
|
|
|
The lock can be acquired using the context manager protocol, and thus idiomatic |
|
use is in conjunction with a with-statement. For example, given derived class T:: |
|
|
|
with T() as t: |
|
t.x = t.frob(t.y) |
|
|
|
All access to the singleton from the outside should follow this pattern for both |
|
attributes and method calls. Singleton members can assume that self is locked by |
|
the caller while they're executing, but recursive locking of the same singleton |
|
on the same thread is also permitted. |
|
""" |
|
|
|
threadsafe_attrs = frozenset() |
|
"""Names of attributes that are guaranteed to be used in a thread-safe manner. |
|
|
|
This is typically used in conjunction with share() to simplify synchronization. |
|
""" |
|
|
|
readonly_attrs = frozenset() |
|
"""Names of attributes that are readonly. These can be read without locking, but |
|
cannot be written at all. |
|
|
|
Every derived class gets its own separate set. Thus, for any given singleton type |
|
T, an attribute can be made readonly after setting it, with T.readonly_attrs.add(). |
|
""" |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
|
|
type(self).readonly_attrs = set(type(self).readonly_attrs) |
|
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod |
|
def assert_locked(self): |
|
lock = type(self)._lock |
|
assert lock.acquire(blocking=False), ( |
|
"ThreadSafeSingleton accessed without locking. Either use with-statement, " |
|
"or if it is a method or property, mark it as @threadsafe_method or with " |
|
"@autolocked_method, as appropriate." |
|
) |
|
lock.release() |
|
|
|
def __getattribute__(self, name): |
|
value = object.__getattribute__(self, name) |
|
if name not in (type(self).threadsafe_attrs | type(self).readonly_attrs): |
|
if not getattr(value, "is_threadsafe_method", False): |
|
ThreadSafeSingleton.assert_locked(self) |
|
return value |
|
|
|
def __setattr__(self, name, value): |
|
assert name not in type(self).readonly_attrs, "This attribute is read-only." |
|
if name not in type(self).threadsafe_attrs: |
|
ThreadSafeSingleton.assert_locked(self) |
|
return object.__setattr__(self, name, value) |
|
|
|
|
|
def threadsafe_method(func): |
|
"""Marks a method of a ThreadSafeSingleton-derived class as inherently thread-safe. |
|
|
|
A method so marked must either not use any singleton state, or lock it appropriately. |
|
""" |
|
|
|
func.is_threadsafe_method = True |
|
return func |
|
|
|
|
|
def autolocked_method(func): |
|
"""Automatically synchronizes all calls of a method of a ThreadSafeSingleton-derived |
|
class by locking the singleton for the duration of each call. |
|
""" |
|
|
|
@functools.wraps(func) |
|
@threadsafe_method |
|
def lock_and_call(self, *args, **kwargs): |
|
with self: |
|
return func(self, *args, **kwargs) |
|
|
|
return lock_and_call |
|
|