web-dev-qa-db-ja.com

このシングルトン実装が「スレッドセーフではない」のはなぜですか?

1. @ Singletonデコレータ

Pythonクラスを装飾してsingletonにするエレガントな方法を見つけました。クラスは1つのオブジェクトしか生成できません。各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)

私はここにコードを見つけました: シングルトンを定義するためのシンプルでエレガントな方法はありますか?

上部のコメントは次のように述べています。

[これは]シングルトンの実装を容易にするための非スレッドセーフなヘルパークラスです。

残念ながら、私は「スレッドの安全性」を自分で確認するのに十分なマルチスレッドの経験がありません。


2.質問

私はこれを使用しています@SingletonマルチスレッドPythonアプリケーションのデコレータ。潜在的な安定性の問題が心配です。したがって:

  1. このコードを完全にスレッドセーフにする方法はありますか?

  2. 前の質問に解決策がない場合(またはその解決策が面倒な場合)、安全を確保するためにどのような予防措置を講じる必要がありますか?

  3. @ Aran-Feyは、デコレータのコーディングが不適切であると指摘しました。もちろん、どんな改善も大歓迎です。


これにより、現在のシステム設定を提供します。
> Python 3.6.3
> Windows 10、64ビット

5
K.Mulier

より良いシングルトン実装を選択することをお勧めします。 メタクラスベースの実装 が最も頻繁に使用されます。

スレッドセーフに関しては、あなたのアプローチも上記のリンクで提案されているものもスレッドセーフではありません:スレッドが既存のインスタンスがないことを読み取って作成を開始する可能性は常にありますが、 別のスレッド同じことをします 最初のインスタンスが保存される前。

この回答で提案されているデコレータ を使用して、メタクラスベースのシングルトンクラスの__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
9

パフォーマンスが心配な場合は、 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
se7entyse7en

@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 
0
martin-voj