Format: text/x-python
⬇ Download File (7.7 KB)
   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