简单来说,单例模式就是保证项目整个生命周期只有一个例子,在项目的任何位置使用都是同一个例子。
虽然单例模式很简单,但还是有一些门道,很少有人知道。
边界情况
Python中实现单例模式的方法有很多,我以前最常用的应该是以下写法。
classSingleton(object): _instance=None def__new__(cls,*args,**kw): ifcls._instanceisNone: cls._instance=object.__new__(cls,*args,**kw) returncls._instance
这种写法有两个问题。
1.单例模式对应类实例化时不能传入参数,将上述代码扩展到以下形式。
classSingleton(object): _instance=None def__new__(cls,*args,**kw): ifcls._instanceisNone: cls._instance=object.__new__(cls,*args,**kw) returncls._instance def__init(self,x,y): self.x=x self.y=y s=Singleton(1,2)
此时,Typeeroror将被抛出: object.__new__() takes exactly one argument (the type to instantiate)错误
2.多线程实例化Singleton时,可能会创建多个实例,因为很有可能多线程同时判断clss._instance is None,从而进入初
在开始实例的代码中。
单例基于同步锁实现
首先考虑上述实现中遇到的第二个问题。
由于在多线程条件下会出现边界条件,从而参数多个例子,因此可以使用同步锁来解决多线程冲突。
importthreading #同步锁 defsynchronous_lock(func): defwrapper(*args,**kwargs): withthreading.Lock(): returnfunc(*args,**kwargs) returnwrapper classSingleton(object): instance=None @synchronous_lock def__new__(cls,*args,**kwargs): ifcls.instanceisNone: cls.instance=object.__new__(cls,*args,**kwargs) returncls.instance
在上述代码中,通过threading.Lock()同步单例化方法,这样在面对多个线程时就不会创建多个例子,可以简单的测试一下。
defworker(): s=Singleton() print(id(s)) deftest(): task=[] foriinrange(10): t=threading.Thread(target=worker) task.append(t) foriintask: i.start() foriintask: i.join() test()
运行后,打印的单例id相同。
更优的方法
添加同步锁后,除了无法输入参数外,没有大问题,但是有更好的解决方案吗?单例模式中是否有可接受参数的实现方法?
defsingleton(cls): cls.__new_original__=cls.__new__ @functools.wraps(cls.__new__) defsingleton_new(cls,*args,**kwargs): it=cls.__dict__.get('__it__') ifitisnotNone: returnit cls.__it__=it=cls.__new_original__(cls,*args,**kwargs) it.__init_original__(*args,**kwargs) returnit cls.__new__=singleton_new cls.__init_original__=cls.__init__ cls.__init__=object.__init__ returncls @singleton classFoo(object): def__new__(cls,*args,**kwargs): cls.x=10 returnobject.__new__(cls) def__init__(self,x,y): assertself.x==10 self.x=x self.y=y
上述代码定义了singleton装饰器,装饰器将在预编译时执行。利用这一特性,singleton装饰器替换了原始__new__与
__init___________________________________________________________________it__属性,以此来判断
是否要创建新的例子,如果要创建,调用原来的__new____________________init__方法将参数传递给当前类,以完成订单
示例模式的目的。
这种方法允许单个示例类接受相应的参数,但面对多线程同时实例化,可能会出现多个示例。此时,可以添加线程同步锁。
defsingleton(cls): cls.__new_original__=cls.__new__ @functools.wraps(cls.__new__) defsingleton_new(cls,*args,**kwargs): #同步锁 withthreading.Lock(): it=cls.__dict__.get('__it__') ifitisnotNone: returnit cls.__it__=it=cls.__new_original__(cls,*args,**kwargs) it.__init_original__(*args,**kwargs) returnit cls.__new__=singleton_new cls.__init_original__=cls.__init__ cls.__init__=object.__init__ returncls
额外考虑是否添加同步锁。
如果一个项目不需要使用线程相关机制,只需要在单个例子中使用线程锁,这实际上是不必要的,它会减慢项目的运行速度。
阅读与CPython线程模块相关的源代码,您会发现Python一开始并没有与初始线程相关的环境,只有当您使用theading库相关的功能时,
将PyEval_Initthreads方法初始化多线程相关环境,代码片段如下(我省略了很多无关代码)。
staticPyObject* thread_PyThread_start_new_thread(PyObject*self,PyObject*fargs) { PyObject*func,*args,*keyw=NULL; structbootstate*boot; unsignedlongident; //多线程环境初始化,解释器默认不初始化,只有用户使用时才初始化。 PyEval_InitThreads();/*Starttheinterpreter'sthread-awareness*/ ///创建线程 ident=PyThread_start_new_thread(t_bootstrap,(void*)boot); //返回线程id returnPyLong_FromUnsignedLong(ident); }
为什么会这样?
由于多线程环境将启动GIL锁相关逻辑,这将影响Python程序的运行速度。许多简单的Python程序不需要使用多线程。此时,它们不需要初始化与线程相关的环境。Python程序在没有GIL锁的情况下运行得更快。
若您的项目不涉及多线程操作,则无需使用同步锁实现单例模式。
结尾
互联网上有很多关于Python实现单例模式的文章。您只需要从多线程下是否可以保证单例和单例化时是否可以输入初始参数来判断
可以采用相应的实现方法。