1 # Copyright (c) Microsoft Corporation. All rights reserved. 2 # Licensed under the MIT License. See LICENSE in the project root 3 # for license information. 4 5 import functools 6 import threading 7 8 9 class Singleton(object): 10 """A base class for a class of a singleton object. 11 12 For any derived class T, the first invocation of T() will create the instance, 13 and any future invocations of T() will return that instance. 14 15 Concurrent invocations of T() from different threads are safe. 16 """ 17 18 # A dual-lock scheme is necessary to be thread safe while avoiding deadlocks. 19 # _lock_lock is shared by all singleton types, and is used to construct their 20 # respective _lock instances when invoked for a new type. Then _lock is used 21 # to synchronize all further access for that type, including __init__. This way, 22 # __init__ for any given singleton can access another singleton, and not get 23 # deadlocked if that other singleton is trying to access it. 24 _lock_lock = threading.RLock() 25 _lock = None 26 27 # Specific subclasses will get their own _instance set in __new__. 28 _instance = None 29 30 _is_shared = None # True if shared, False if exclusive 31 32 def __new__(cls, *args, **kwargs): 33 # Allow arbitrary args and kwargs if shared=False, because that is guaranteed 34 # to construct a new singleton if it succeeds. Otherwise, this call might end 35 # up returning an existing instance, which might have been constructed with 36 # different arguments, so allowing them is misleading. 37 assert not kwargs.get("shared", False) or (len(args) + len(kwargs)) == 0, ( 38 "Cannot use constructor arguments when accessing a Singleton without " 39 "specifying shared=False." 40 ) 41 42 # Avoid locking as much as possible with repeated double-checks - the most 43 # common path is when everything is already allocated. 44 if not cls._instance: 45 # If there's no per-type lock, allocate it. 46 if cls._lock is None: 47 with cls._lock_lock: 48 if cls._lock is None: 49 cls._lock = threading.RLock() 50 51 # Now that we have a per-type lock, we can synchronize construction. 52 if not cls._instance: 53 with cls._lock: 54 if not cls._instance: 55 cls._instance = object.__new__(cls) 56 # To prevent having __init__ invoked multiple times, call 57 # it here directly, and then replace it with a stub that 58 # does nothing - that stub will get auto-invoked on return, 59 # and on all future singleton accesses. 60 cls._instance.__init__() 61 cls.__init__ = lambda *args, **kwargs: None 62 63 return cls._instance 64 65 def __init__(self, *args, **kwargs): 66 """Initializes the singleton instance. Guaranteed to only be invoked once for 67 any given type derived from Singleton. 68 69 If shared=False, the caller is requesting a singleton instance for their own 70 exclusive use. This is only allowed if the singleton has not been created yet; 71 if so, it is created and marked as being in exclusive use. While it is marked 72 as such, all attempts to obtain an existing instance of it immediately raise 73 an exception. The singleton can eventually be promoted to shared use by calling 74 share() on it. 75 """ 76 77 shared = kwargs.pop("shared", True) 78 with self: 79 if shared: 80 assert ( 81 type(self)._is_shared is not False 82 ), "Cannot access a non-shared Singleton." 83 type(self)._is_shared = True 84 else: 85 assert type(self)._is_shared is None, "Singleton is already created." 86 87 def __enter__(self): 88 """Lock this singleton to prevent concurrent access.""" 89 type(self)._lock.acquire() 90 return self 91 92 def __exit__(self, exc_type, exc_value, exc_tb): 93 """Unlock this singleton to allow concurrent access.""" 94 type(self)._lock.release() 95 96 def share(self): 97 """Share this singleton, if it was originally created with shared=False.""" 98 type(self)._is_shared = True 99 100 101 class ThreadSafeSingleton(Singleton): 102 """A singleton that incorporates a lock for thread-safe access to its members. 103 104 The lock can be acquired using the context manager protocol, and thus idiomatic 105 use is in conjunction with a with-statement. For example, given derived class T:: 106 107 with T() as t: 108 t.x = t.frob(t.y) 109 110 All access to the singleton from the outside should follow this pattern for both 111 attributes and method calls. Singleton members can assume that self is locked by 112 the caller while they're executing, but recursive locking of the same singleton 113 on the same thread is also permitted. 114 """ 115 116 threadsafe_attrs = frozenset() 117 """Names of attributes that are guaranteed to be used in a thread-safe manner. 118 119 This is typically used in conjunction with share() to simplify synchronization. 120 """ 121 122 readonly_attrs = frozenset() 123 """Names of attributes that are readonly. These can be read without locking, but 124 cannot be written at all. 125 126 Every derived class gets its own separate set. Thus, for any given singleton type 127 T, an attribute can be made readonly after setting it, with T.readonly_attrs.add(). 128 """ 129 130 def __init__(self, *args, **kwargs): 131 super().__init__(*args, **kwargs) 132 # Make sure each derived class gets a separate copy. 133 type(self).readonly_attrs = set(type(self).readonly_attrs) 134 135 # Prevent callers from reading or writing attributes without locking, except for 136 # reading attributes listed in threadsafe_attrs, and methods specifically marked 137 # with @threadsafe_method. Such methods should perform the necessary locking to 138 # ensure thread safety for the callers. 139 140 @staticmethod 141 def assert_locked(self): 142 lock = type(self)._lock 143 assert lock.acquire(blocking=False), ( 144 "ThreadSafeSingleton accessed without locking. Either use with-statement, " 145 "or if it is a method or property, mark it as @threadsafe_method or with " 146 "@autolocked_method, as appropriate." 147 ) 148 lock.release() 149 150 def __getattribute__(self, name): 151 value = object.__getattribute__(self, name) 152 if name not in (type(self).threadsafe_attrs | type(self).readonly_attrs): 153 if not getattr(value, "is_threadsafe_method", False): 154 ThreadSafeSingleton.assert_locked(self) 155 return value 156 157 def __setattr__(self, name, value): 158 assert name not in type(self).readonly_attrs, "This attribute is read-only." 159 if name not in type(self).threadsafe_attrs: 160 ThreadSafeSingleton.assert_locked(self) 161 return object.__setattr__(self, name, value) 162 163 164 def threadsafe_method(func): 165 """Marks a method of a ThreadSafeSingleton-derived class as inherently thread-safe. 166 167 A method so marked must either not use any singleton state, or lock it appropriately. 168 """ 169 170 func.is_threadsafe_method = True 171 return func 172 173 174 def autolocked_method(func): 175 """Automatically synchronizes all calls of a method of a ThreadSafeSingleton-derived 176 class by locking the singleton for the duration of each call. 177 """ 178 179 @functools.wraps(func) 180 @threadsafe_method 181 def lock_and_call(self, *args, **kwargs): 182 with self: 183 return func(self, *args, **kwargs) 184 185 return lock_and_call 186