15

1. The @Singleton decorator

I found an elegant way to decorate a Python class to make it a singleton. The class can only produce one object. Each Instance() call returns the same object:

class Singleton:
    """
    A non-thread-safe helper class to ease implementing singletons.
    This should be used as a decorator -- not a metaclass -- to the
    class that should be a singleton.

    The decorated class can define one `__init__` function that
    takes only the `self` argument. Also, the decorated class cannot be
    inherited from. Other than that, there are no restrictions that apply
    to the decorated class.

    To get the singleton instance, use the `Instance` method. Trying
    to use `__call__` will result in a `TypeError` being raised.

    """

    def __init__(self, decorated):
        self._decorated = decorated

    def Instance(self):
        """
        Returns the singleton instance. Upon its first call, it creates a
        new instance of the decorated class and calls its `__init__` method.
        On all subsequent calls, the already created instance is returned.

        """
        try:
            return self._instance
        except AttributeError:
            self._instance = self._decorated()
            return self._instance

    def __call__(self):
        raise TypeError('Singletons must be accessed through `Instance()`.')

    def __instancecheck__(self, inst):
        return isinstance(inst, self._decorated)

I found the code here: Is there a simple, elegant way to define singletons?

The comment at the top says:

[This is] a non-thread-safe helper class to ease implementing singletons.

Unfortunately, I don't have enough multithreading experience to see the 'thread-unsafeness' myself.

 

2. Questions

I'm using this @Singleton decorator in a multithreaded Python application. I'm worried about potential stability issues. Therefore:

  1. Is there a way to make this code completely thread-safe?

  2. If the previous question has no solution (or if its solution is too cumbersome), what precautions should I take to stay safe?

  3. @Aran-Fey pointed out that the decorator is badly coded. Any improvements are of course very much appreciated.


Hereby I provide my current system settings:
    >  Python 3.6.3
    >  Windows 10, 64-bit

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
K.Mulier
  • 8,069
  • 15
  • 79
  • 141
  • 2
    Thanks for including the link to the original question; makes it easy to go downvote that answer... But seriously, that's a bad decorator. – Aran-Fey May 28 '18 at 12:53
  • It does not seem to work. – Olivier Melançon May 28 '18 at 12:54
  • Hi @Aran-Fey, thank you for pointing that out. Please feel free to make improvements to the decorator. I would greatly appreciate that :-) – K.Mulier May 28 '18 at 12:54
  • Hi @OlivierMelançon, what exactly is not working? It (it = the decorator) seems to work on my system (but maybe I'm missing something here). But as Aran-Fey just pointed out, perhaps the decorator should be improved :-) – K.Mulier May 28 '18 at 12:55
  • @K.Mulier It actually works... I just find it weird to have to call the Instance() method to get the singleton. I suggest you have a look at [this question](https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python) that displays neater and more accepted ways to have singletons. Since they are more broadly used, you will find it easier to get information about their thread-safety. – Olivier Melançon May 28 '18 at 13:00
  • You can find better singletons [here](https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python). – Aran-Fey May 28 '18 at 13:02
  • Thank you :-). I just looked at the page. It's bloody long with lots of options... what method do you personally prefer to make singletons? What about its thread-safety? – K.Mulier May 28 '18 at 13:06
  • @K.Mulier The metaclass approach is the most commonly used. As for the thread-safety... my guess is that none of them are. – Olivier Melançon May 28 '18 at 13:08

4 Answers4

24

I suggest you choose a better singleton implementation. The metaclass-based implementation is the most elegant in my opinion.

As for for thread-safety, neither your approach nor any of the ones suggested in the above link are thread safe: it is always possible that a thread reads that there is no existing instance and starts creating one, but another thread does the same before the first instance was stored.

You can use a with lock controller to protect the __call__ method of a metaclass-based singleton class with a lock.

import threading

lock = threading.Lock()

class Singleton(type):
    _instances = {}

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


class SingletonClass(metaclass=Singleton):
    pass

As suggested by se7entyse7en, you can use a check-lock-check pattern. Since singletons are only created once, your only concern is that the creation of the initial instance must be locked. Although once this is done, retrieving the instance requires no lock at all. For that reason we accept the duplication of the check on the first call so that all further call do not even need to acquire the lock.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • Great! So you combined the `metaclass-based singleton implementation` with the `threading lock`, making the whole thing thread-safe, right? – K.Mulier May 28 '18 at 13:32
  • @K.Mulier yes, you got it – Olivier Melançon May 28 '18 at 13:33
  • This isn't working for me. The class that I am trying to make a singleton is being creating multiple times, depending on where else in my code it gets called. I realized that one of those calls is coming out of code that is called in a thread, which is why I tried this code, but it doesn't seem to have any different effect. Did this code work? Did anyone make changes to it? – Terri Simon Feb 07 '19 at 21:27
  • @TerriSimon We would need to see your code, I recommend you ask a new question. link it to this one stating that it didn't work and share your code so people can have enough information to help. – Olivier Melançon Feb 13 '19 at 03:37
  • @OlivierMelançon - Thanks but I figured out my problem a different way. What I believe was happening was that my code was getting called, not from just different threads but from different processes. I had to go back up the chain to see how the things that were calling the singleton were getting created. I wound up not using the singleton after I did some rearrangement of code to improve the whole set of functionality. – Terri Simon Feb 13 '19 at 11:16
  • why the need of double `if` checking? is it really needed or it's a mistake? – Troopers Jun 17 '20 at 12:25
  • 1
    @Troopers it's a [check-lock-check pattern](https://en.m.wikipedia.org/wiki/Double-checked_locking). It is explained below the code in my answer. – Olivier Melançon Jun 17 '20 at 14:25
  • This implementation has a subtle "issue" that you should be aware of. – user1720902 Nov 22 '22 at 23:46
  • @user1720902 Please explain – Olivier Melançon Nov 23 '22 at 00:04
11

If you're concerned about performance you could improve the solution of the accepted answer by using the check-lock-check pattern to minimize locking acquisition:

class SingletonOptmized(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._locked_call(*args, **kwargs)
        return cls._instances[cls]

    @synchronized(lock)
    def _locked_call(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(SingletonOptmized, cls).__call__(*args, **kwargs)

class SingletonClassOptmized(metaclass=SingletonOptmized):
    pass

Here's the difference:

In [9]: %timeit SingletonClass()
488 ns ± 4.67 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [10]: %timeit SingletonClassOptmized()
204 ns ± 4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
se7entyse7en
  • 4,310
  • 7
  • 33
  • 50
4

I'm posting this just to simplify suggested solution by @OlivierMelançon and @se7entyse7en: no overhead by import functools and wrapping.

import threading

lock = threading.Lock()

class SingletonOptmizedOptmized(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            with lock:
                if cls not in cls._instances:
                    cls._instances[cls] = super(SingletonOptmizedOptmized, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class SingletonClassOptmizedOptmized(metaclass=SingletonOptmizedOptmized):
    pass

Difference:

>>> timeit('SingletonClass()', globals=globals(), number=1000000)
0.4635776
>>> timeit('SingletonClassOptmizedOptmized()', globals=globals(), number=1000000)
0.192263300000036
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
martin-voj
  • 192
  • 3
  • 12
4

While providing thread-safety, the currently accepted answer has limitation as it can easily dead-lock.

For example, if both Class_1 and Class_2 implement that singleton pattern, calling the constructor of Class_1 in Class_2 (or vice versa) would dead-lock. This is due to the fact that all the classes implemented through that meta-class share the same lock.

After searching the internet for a better design, I found this one:

enter image description here

https://gist.github.com/wys1203/830f52c31151226599ac015b87b6e05c

It overcomes the dead-lock limitation by providing each class implemented through the meta-class with its own lock.

Guett31
  • 258
  • 2
  • 15