为什么这个单例实现“不是线程安全的”?

时间:2018-05-28 12:50:52

标签: python multithreading python-3.x singleton python-3.6

1。 @Singleton装饰者

我找到了一种优雅的方法来装饰Python类,使其成为singleton。该类只能生成一个对象。每个Instance()调用都返回相同的对象:

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)

我在这里找到了代码: Is there a simple, elegant way to define singletons?

顶部的评论说:

  

[这是]一个非线程安全的助手类,可以轻松实现单例。

不幸的是,我没有足够的多线程经验来自己看到'线程不安全'。


2。问题

我在多线程Python应用程序中使用此@Singleton装饰器。我担心潜在的稳定性问题。因此:

  1. 有没有办法让这段代码完全是线程安全的?

  2. 如果上一个问题没有解决方案(或者解决方案过于繁琐),我应采取哪些预防措施以保证安全?

  3. @Aran-Fey指出装饰器编码错误。当然,非常感谢任何改进。

  4. 特此提供我当前的系统设置:
    > Python 3.6.3
    > Windows 10,64位

3 个答案:

答案 0 :(得分:7)

我建议你选择一个更好的单例实现。 metaclass-based implementation是最常用的。

至于线程安全,你的方法或上面链接中建议的任何方法都不是线程安全的:线程总是可以读取没有现有实例并开始创建一个,但是{{3在存储第一个实例之前。

您可以使用another thread does the same来保护基于元类的单例类的__call__方法。

import functools
import threading

lock = threading.Lock()


def synchronized(lock):
    """ Synchronization decorator """
    def wrapper(f):
        @functools.wraps(f)
        def inner_wrapper(*args, **kw):
            with lock:
                return f(*args, **kw)
        return inner_wrapper
    return wrapper


class Singleton(type):
    _instances = {}

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


class SingletonClass(metaclass=Singleton):
    pass

答案 1 :(得分:0)

如果您担心性能,可以使用check-lock-check pattern来最大程度地减少锁定获取,从而改善已接受答案的解决方案:

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

区别在于:

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)

答案 2 :(得分:0)

我发布此信息只是为了简化@OlivierMelançon和@ se7entyse7en建议的解决方案:import functools和包装不会产生开销。

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

差异:

>>> timeit('SingletonClass()', globals=globals(), number=1000000)
0.4635776
>>> timeit('SingletonClassOptmizedOptmized()', globals=globals(), number=1000000)
0.192263300000036