C#中的深度

实现Singleton模式在C#

表的内容(连接)

介绍

Singleton模式在软件工程中最知名的图案之一。从本质上讲,一个单身是一类只允许本身的一个实例被创建,并提供了简单的访问到该实例的。最常见的,单身不容许任何的实例被创建时指定的参数 - 否则第二个请求的实例,但使用不同的参数可能会出现问题!(如果同一个实例可以被访问的所有请求都使用相同的参数,工厂模式是比较合适的。)本文只涉及不需要任何参数的情况下。通常情况下,单身的要求是,他们创建懒洋洋地 - 即,不会创建实例之前,它首先需要。

有各种不同的方式在C#中实现Singleton模式。我将在这里展示他们在相反的顺序,优雅,最常见的,这是不是线程安全的,并充分懒洋洋地加载,线程安全的,简单的和高性能的版本。

所有这些实现共享4个共同的特点,但是:

  • 一个构造函数,这是私人和参数的。这可以防止其他类中实例化它(这将是一个违反的格局)。请注意,它也可以防止子类化 - 如果一个单身一次可以被子类化,它可以被子类化的两倍,如果这些子类可以创建一个实例,该模式被侵犯。如果你只需要一个实例的基本类型,可以使用工厂模式,但不知道,直到运行时的确切类型。
  • 类是密封的。这是不必要的,严格来说,由于上述的点,但可能帮助JIT事情更加优化。
  • 静态变量保存了一个引用到创建的实例,如果有的话。
  • 创建的实例参考,如有必要,创建一个公共的静态方法。

需要注意的是,所有这些实现也可以使用一个公共静态属性 的方式访问实例的实例。在所有情况下,可以很容易地转换为方法,线程安全性或性能没有影响。

第一个版本 - 不是线程安全的

/ /错误代码!不要使用
公共  密封    的Singleton 
{ 私人静态  Singleton实例=  ; 私人  的Singleton()     {     } 公共静态  Singleton实例     {         得到        { 如果  (实例==              {                 =   辛格尔顿();             } 实例   的实例;        }     } } 
     

    



     



            



            

 

在之前所暗示的,上面是不是线程安全的。两个不同的线程可以同时评估测试(例如== NULL) ,发现它是真实的,那么这两个创建实例,这违反了Singleton模式。请注意,实际上该实例可能已经被创建的表达式求值之前,但并不能保证被其他线程的实例将被视为新的价值,除非已通过适当的记忆障碍的内存模型。

第二个版本 - 简单的线程安全

公共  密封    的Singleton 
{ 私人静态  Singleton实例=   ; 私人静态只读对象  挂锁的  对象();     辛格尔顿()     {     } 公共静态 Singleton实例     {         {   (挂锁)             { 如果  (实例==                   {                    例如=    辛格尔顿();                 } 返回  的实例;             }         }     } } 
     
        





     



            

                



                


 

这个实现是线程安全的。线程需要共享对象上的锁,然后检查是否已创建的实例,然后再创建实例。这需要护理的记忆障碍问题(如锁定确保所有读取数据发生逻辑锁之后的收购,和解锁,确保所有的写操作发生逻辑上的锁释放前),并确保只有一个线程将创建一个实例(如只一个线程可以在这部分的代码的时间 - 的时候,进入第二个线程,第一个线程将创建实例,所以该表达式将计算结果为false)。不幸的是,性能会受到影响锁定为获得每次请求的实例。

需要注意的是,而不是锁定typeof运算(单身),某些版本的这个实现的话,我这是私人的类的静态变量的值锁定。锁定对象的其他类可以访问并锁定(如类型)的性能问题与风险,甚至死锁。这是我的一个概括的风格偏好-在可能的情况下,只有在对象上的锁专门创建的目的,或锁定的文件,他们是被锁定在作特定用途(例如队列等待/脉冲)。通常,这样的对象应该是私有的类。这有助于显着编写线程安全的应用程序更容易使用它们。

第三个版本 - 企图使用双重检查锁定的线程安全

/ /错误代码!不要使用
公共  密封    的Singleton 
{ 私人静态  Singleton实例=   ; 私人静态只读对象  挂锁的=  新的对象();     辛格尔顿()     {     } 公共静态的 Singleton实例     {         { 如果  (实例==               {   (挂锁)                 { 如果  (实例==  NULL                     {                         例如   辛格尔顿();                     }                }             } 返回  的实例;         }     } } 
     
        





     



            

                

                    





            

 

这的实现尝试是线程安全的必要性,每次锁的情况下。不幸的是,有4到图案缺点:

  • 在Java中,这是行不通的。这似乎是一个奇怪的事情发表评论,但它是值得了解的,如果你需要的Singleton模式在Java和C#程序员,也可能是Java程序员。Java内存模型并不能保证前到新的对象被分配到实例构造函数完成。1.5版本的Java内存模型进行了改造,但仍然是断开的双重检查锁定后没有volatile变量(如C#)。
  • 没有任何记忆障碍,它打破了在ECMA CLI规范。这是可能的。NET 2.0的内存模型(这是比ECMA规范)下是安全的,但我宁愿不依赖于那些更强的语义,特别是如果有任何疑问的安全。实例变量volatile可以使工作,将明确的内存屏障电话,虽然在后一种情况下,即使是专家不同意哪些障碍需要。我倾向于尽量避免情况下,专家们不同意什么是正确的,什么是错的!
  • 这很容易出错。该模式需要要漂亮得多就像上面那样 - 任何重大变化可能会影响性能或正确性。
  • 它仍然没有执行,以及后来的实现。

第四个版本 - 不是很懒惰,但线程安全的,而无需使用锁

公共  密封    的Singleton 
{ 私人静态只读  的Singleton的实例=    辛格尔顿(); / /显式的静态构造函数,以告诉C#编译器/ /未标记类型beforefieldinit 静态  的Singleton()     {     } 私人  的Singleton()     {     } 公共静态  的Singleton实例    {         { 返回  实例;         }     } } 
      

    
    
    



    



     



            

 

正如你可以看到,这确实是非常简单 - 但为什么它是线程安全的,如何懒惰呢?好了,在C#中的静态构造函数中指定执行,只有当一个类的实例被创建或引用静态成员,每个AppDomain只执行一次。检查新建成的类型,这需要执行,无论发生了什么,这样速度会更快,比在前面的例子中增加额外的检查。有一对夫妇的皱纹,但是:

  • 这不是因为懒的其他实现。特别是,如果你有静态成员以外的其他实例中,首次提到这些成员将包括创建实例。这在接下来的实施校正。
  • 有并发症,如果一个静态构造函数调用另一个调用第一个。看。NET技术规范(目前第9.5.3分区II)的确切性质的类型初始值设定项的更多细节 - 他们是不会咬你的,但它是值得了解的后果静态构造函数是指每个在一个周期内的其他。
  • 懒惰的类型初始值设定项时,只保证。NET的类型没有打上一个特殊的标志,称为beforefieldinit。不幸的是,C#编译器(如所提供的。NET 1.1运行时,至少)标记的所有类型,没有一个静态构造函数(即一个块,它看起来像是一个构造函数,但被标记为静态)beforefieldinit。我现在有一个文章有关此问题的更多详细信息。另外请注意,对性能的影响,所讨论的页面底部附近。

一个快捷方式,你可以把这个实现(仅此一家),就是让 例如一个公共静态只读变量,得到的财产完全摆脱。这使得绝对小的基本骨架代码!然而,很多人喜欢有一个属性的情况下,需要采取进一步的行动在未来,和JIT内联是可能的表现是相同的。(请注意,如果您需要懒惰的静态构造函数本身仍然需要)。

第五版本 - 完全懒实例

公共  密封    的Singleton,
{ 私人  的Singleton()     {     } 公共静态  Singleton实例{ 返回  Nested.instance;}}  嵌套     { / /显式的静态构造函数来告诉C#编译器/ /不标记类型beforefieldinit 静态  嵌套()         {         } 的静态只读  Singleton实例=    辛格尔顿();     } } 
    



     
        
     

        
        
        



          
 

在这里,被触发的第一个引用的嵌套类的静态成员,只发生在实例的实例。这意味着实施是完全迟缓,但有以前的所有的性能优势。请注意,虽然嵌套类的访问封装类的私有成员,相反是不正确的,因此需要例如这里是内部的。这并不提出任何其他的问题,不过,作为类本身是私人的。该代码是一个比较复杂一点,为了使懒惰的实例。

第六版-使用。NET 4的懒惰<T>类型

如果你使用。NET 4(或更高),您可以使用System.Lazy的<T> 类型懒惰,很简单的。所有你需要做的是通过委托的构造函数调用Singleton的构造函数-这是最容易做一个lambda表达式。

公共  密封    的Singleton 
{ 私人静态只读  懒惰的<Singleton>懒=  懒惰<Singleton> 新的(()=>    辛格尔顿()); 公共静态  Singleton实例{返回  lazy.Value;}} 私人  的Singleton()     {     } } 
      
        
    
     

    

 

这很简单,性能良好。它也可以让你检查是否或不是实例已创建的IsValueCreated 属性的,如果你需要的。

性能与懒惰

在许多情况下,你实际上并不需要完整的懒惰 - 除非你的类的初始化做了特别费时,或其他地方有一些副作用,这可能是罚款离开了明确的静态构造函数。这可以提高性能,因为它可以让JIT编译器进行一个单一的检查,以确保已初始化的类型(例如在一个方法的开始),则假设其从那时起。如果你的单身实例是在一个相对紧密的循环引用,这可以使一个显着的性能差异(相对)。你应该决定是否或不完全懒实例化是必需的,在类中,适当地记下此决定。

此页的存在的原因是很多人是聪明的,因此,未来的双重检查锁定算法。有一个锁定是昂贵的,这是常见的和误导的态度。我写了一个非常快速的基准刚刚获得的单身实例的在一个循环十亿方式,尝试不同的变体。这不是非常科学的,因为在现实生活中,你可能想知道如何快速的是,如果每一次迭代需要调用一个方法获取的单身等,但它确实显示出重要的一点。在我的笔记本电脑,最慢的解决方案的5倍,是锁定1(方案二)。这重要吗?也许不是,当你记住,它仍然设法在40秒内获得的单身一亿次。(注:这篇文章原本是前一段写的-现在我期待更好的性能。)这意味着,如果你是“只”取得的单身四十万次每秒,收购的成本是怎么回事要提高1%的性能-它是不会做了很多。现在,如果你 收购单身,经常-是不是有可能你正在使用它在一个循环中吗?如果你很在乎性能提高一点点,为什么不声明局部变量外循环,收购的单身一次,然后循环。宾果游戏,即使是最慢的实施变得容易足够。

我会很有兴趣地看到一个真实世界的应用程序,使用简单的锁定之间的区别和使用一个更快的解决方案实际上是一个显着的性能差异。

例外

有时候,你需要做的工作,在一个单独的构造函数可能抛出一个异常,但可能不会是致命的整个应用程序。可能,您的应用程序可能能够解决的问题,并希望再次尝试。在这个阶段,使用类型初始构建的单身的问题。不同的运行时处理这种情况有所不同,但我不知道任何所需的东西(再次运行类型初始值设定项),即使一个人,你的代码将被打破在其他的运行时间。为了避免这些问题,我建议你使用第二页上列出的模式 - 只需要使用一个简单的锁,并通过检查,每次去建设中的实例的方法/属性,如果没有已经成功地建立。

感谢舍甫琴科捷列先科提出这个问题。

结论(2006年1月7日小幅修改,更新了2011年2月12日)

有各种不同的方式在C#中实现Singleton模式。一位读者写了给我详细介绍了他的方式封装的同步方面,可能是有用的,而我承认在一些非常特别的情况下(具体在哪里,你需要非常高的性能能力,以确定是否单身已经创建,懒惰,无论静态成员被调用)。

我个人的偏好是解决方案4:我通常会远离它是唯一的一次,如果我需要能够调用其他的static方法,而不会触发初始化,或者如果我需要知道的单身是否已经被实例化。我不记得我最后一次是在这种情况下,我什至有。在这种情况下,我可能会选择的解决方案,这仍然是好的,易于获得的权利。

解决方案是优雅,但超过2个或4个棘手的,正如我上面所说,它提供的好处似乎只有很少有用。解决方案是一个简单的方法来实现的懒惰,如果你使用。NET 4。它也有优势,它的明显懒。我目前倾向于使用的解决方案,只需通过习惯-但如果我工作经验的开发人员,我想我很可能去解决6开始作为一个简单的和普遍适用的模式。

(我不会使用的解决方案,因为它打破了,我不会使用的解决方案,因为它没有超过5)

   复制过来的!。。。。。