You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
7.5 KiB
Python
186 lines
7.5 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the MIT License. See LICENSE in the project root
|
|
# for license information.
|
|
|
|
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.
|
|
"""
|
|
|
|
# A dual-lock scheme is necessary to be thread safe while avoiding deadlocks.
|
|
# _lock_lock is shared by all singleton types, and is used to construct their
|
|
# respective _lock instances when invoked for a new type. Then _lock is used
|
|
# to synchronize all further access for that type, including __init__. This way,
|
|
# __init__ for any given singleton can access another singleton, and not get
|
|
# deadlocked if that other singleton is trying to access it.
|
|
_lock_lock = threading.RLock()
|
|
_lock = None
|
|
|
|
# Specific subclasses will get their own _instance set in __new__.
|
|
_instance = None
|
|
|
|
_is_shared = None # True if shared, False if exclusive
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
# Allow arbitrary args and kwargs if shared=False, because that is guaranteed
|
|
# to construct a new singleton if it succeeds. Otherwise, this call might end
|
|
# up returning an existing instance, which might have been constructed with
|
|
# different arguments, so allowing them is misleading.
|
|
assert not kwargs.get("shared", False) or (len(args) + len(kwargs)) == 0, (
|
|
"Cannot use constructor arguments when accessing a Singleton without "
|
|
"specifying shared=False."
|
|
)
|
|
|
|
# Avoid locking as much as possible with repeated double-checks - the most
|
|
# common path is when everything is already allocated.
|
|
if not cls._instance:
|
|
# If there's no per-type lock, allocate it.
|
|
if cls._lock is None:
|
|
with cls._lock_lock:
|
|
if cls._lock is None:
|
|
cls._lock = threading.RLock()
|
|
|
|
# Now that we have a per-type lock, we can synchronize construction.
|
|
if not cls._instance:
|
|
with cls._lock:
|
|
if not cls._instance:
|
|
cls._instance = object.__new__(cls)
|
|
# To prevent having __init__ invoked multiple times, call
|
|
# it here directly, and then replace it with a stub that
|
|
# does nothing - that stub will get auto-invoked on return,
|
|
# and on all future singleton accesses.
|
|
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)
|
|
# Make sure each derived class gets a separate copy.
|
|
type(self).readonly_attrs = set(type(self).readonly_attrs)
|
|
|
|
# Prevent callers from reading or writing attributes without locking, except for
|
|
# reading attributes listed in threadsafe_attrs, and methods specifically marked
|
|
# with @threadsafe_method. Such methods should perform the necessary locking to
|
|
# ensure thread safety for the callers.
|
|
|
|
@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
|