设计模式基于C#的实现与扩展——创建型模式(四)

4. 单件模式

Ensure a class only has one instance, and provide a global point of access to.
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
《设计模式:可复用面向对象软件的基础》

单件模式是几个创建型模式中最独立的一个,它的主要特点不是根据用户程序调用生产一个新的实例,而是控制某个类型的实例数量——唯一一个。很多时候我们需要在应用中保存一个唯一的实例,比如:网络端口,虽然在一台机器上一个网络端口可以被安排非常多的工作,但最终一个端口在同一时间内只能有一个进程打开。为了防止在“一窝蜂”的情况,某个进程打开后没有很好的关闭端口,导致其他人都无法使用的情况,你可以能需要用一个管理进程来控制端口,其他进程需要向这个管理进行申请服务。那么这个唯一的管理进程就是一个Singleton(单件)。
实现单件的方式很多,但大体上可以划分为两种。

  • 外部方式:客户程序在使用某些全局性(或语义上下文范围内的全局性)的对象时,做些“Try-Create-Store-Use”的工作。如果没有,就自己创建一个,新创建的对象仍然把它搁在那个全局的位置上;如果已有了,就直接拿现成的。
  • 内部方式:类型自己控制生成实例的数量,无论客户程序是否Try过,类型自己就只提供一个实例,客户程序使用的都是这个现成的唯一实例。
    相比较而言,外部方式实在不靠谱,毕竟应用中的类型不止一个,即使某个类型可以恪守这种先Try的方式,仍然可能有其他类型会用到它,很难要求所有相关类型都做到唯一性检查,最终的结果就是无法唯一。内部方式把干扰因素排除在类型之外,相对更保险一下。

经典模式

实际上,Singleton模式要做的就是通过控制类型实例的创建,确保后续使用的都是之前创建好的一个实例,通过这样的一个封装,客户程序就无需知道该类型实现的内部细节。
在逻辑模型上,Singleton模式很直观。
— 类图在此 —
参与者:
Singleton自己,它的职责是定义一个Instance操作,作为客户程序访问Singleton实例的唯一入口。

public class Singleton
{
    private static Singleton instance;
    protected Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (instance == null)
                instance = new Singleton();
            return instance;
        }
    }
}

扩展

上面这段代码使用Lazy方式创建唯一实例,大体上可以满足最初Singleton模式的设计要求,在大多数情况下这段代码也可以很好地工作。
但是在多线程环境下,这种实现方式存在很多潜在的缺陷,最直接的问题就是位于 if 部分,当多个线程几乎同时调用Singleton类的Instance静态属性的时候,instance成员可能还没有被实例化,因此它会被多个线程同时创建多次,而且最终Singleton类保存的是最后创建的那个实例。各个线程引用的对象实例不同,这违背了我们“唯一实例”的初衷。

线程安全的单件

在综合消息和线程同步的考虑后,我们可以采用一种 double check的方式来修改经典模式中的代码。

public class DoubleCheckSingleton
{
    protected DoubleCheckSingleton() { }
    private static volatile DoubleCheckSingleton instance = null;

    public static DoubleCheckSingleton Instance()
    {
        if (instance == null)
        {
            lock (typeof(DoubleCheckSingleton))
                if (instance == null) 
                	instance = new DoubleCheckSingleton();
        }
        return instance;
    }
}

上面的代码,有几点需要注意:

  • lock加在内层的if部分。如果lock加在外层if,客户程序每次调用Instance都需要检查instance是否为空,那么每次执行都需要先lock。但是在绝大多数情况下,运行时这个instance都不为空,每次加锁效率太差,而且可能成为瓶颈。lock加在内层if,等于组成了一个相对线程安全的小环境。
  • volatile关键字,表示此字段可能被多个并发执行线程修改。声明为volatile字段不受编译器优化的限制,这样可以确保该字段在任何时间呈现的都是最新的值。

其实考虑到Singleton实例仅有一个内部静态成员保存,如果它本身的构建过程中不需要依赖其他外部机制或不需要准备很多构造参数,也可以直接用下面的定义方式。

public class CoolSingleton
{
    protected CoolSingleton() { }
    protected static readonly CoolSingleton instance = new CoolSingleton();

    public static CoolSingleton Instance { get { return instance; } }
}

Unit Test

[TestMethod]
public void Test_CoolSingleton()
{
    CoolSingleton s1 = CoolSingleton.Instance;
    CoolSingleton s2 = CoolSingleton.Instance;

    Assert.IsNotNull(s1);
    Assert.IsNotNull(s2);
    Assert.AreSame(s1, s2);
}

[TestMethod]
public void Test_CoolSingleton_ThreadSafe()
{
    int threadCount = 3;
    Thread[] threads = new Thread[threadCount];
    Work[] works = new Work[threadCount];

    for (int i = 0; i < threadCount; i++)
    {
        works[i] = new Work();
        ThreadStart start = new ThreadStart(works[i].Procedure);
        threads[i] = new Thread(start);
    }

    foreach (Thread th in threads) th.Start();
    foreach (Thread th in threads) th.Join();

    for(int i = 0; i< threadCount; i++)
        for (int j = i + 1; j < threadCount; j++)
            Assert.AreSame(works[i].Instance, works[j].Instance);
}

class Work
{
    public CoolSingleton Instance { get; private set; }

    public void Procedure()
    {
        Instance = CoolSingleton.Instance;
        Console.WriteLine(Instance.GetHashCode());
    }
}

静态变量instance在被访问之前,都会在静态构造函数中被实例化,而且它是只读的,因此一旦创建,它就不能被任何线程修改,也就不用做Double Check了。用很简短的代码就实现了一个线程安全的Singleton。很Cool

细粒度的Singleton

前面我们讨论的都是线程安全的单件实现,但是在项目中我们往往需要更细粒度的单件。比如某个线程是长时间运行的后台任务,它本身存在需要模块和中间处理,它希望每个线程都有自己的单独Singleton对象,不同线程独立操作自己线程内的Singleton。

ThreadStatic属性实现

C#程序可以通过把静态成员标示为ThreadStatic,以确保它指示静态字段对于每个线程都是唯一的。
但是标识为ThreadStatic的字段,不能在静态构造函数中实例化,那么我们就不能用之前的很Cool的方式来实现,而需要退回到最经典的Lazy方式加载Singleton的方法。Lazy的方式不是线程安全的,但是我们这里实现的本身就只运行在一个线程中,这种方式反而更符合要求。

public class ThreadSingleton
{
    private ThreadSingleton() { }

    [ThreadStatic]
    private static ThreadSingleton instance;

    public static ThreadSingleton Instance
    {
        get
        {
            if (instance == null) instance = new ThreadSingleton();

            return instance;
        }
    }
}

Unit Test

[TestMethod]
public void Test_ThreadSingleton()
{
    int threadCount = 3;
    Thread[] threads = new Thread[threadCount];
    Work2[] works = new Work2[threadCount];

    for (int i = 0; i < threadCount; i++)
    {
        works[i] = new Work2();
        ThreadStart start = new ThreadStart(works[i].Procedure);
        threads[i] = new Thread(start);
    }

    foreach (Thread th in threads) th.Start();

    foreach (Thread th in threads) th.Join();

    for (int i = 0; i < threadCount; i++)
        for (int j = i + 1; j < threadCount; j++)
            Assert.AreNotSame(works[i].Instance, works[j].Instance);
}

class Work2
{
    public ThreadSingleton Instance { get; private set; }

    public void Procedure()
    {
        Instance = ThreadSingleton.Instance;
        Console.WriteLine(Instance.GetHashCode());
    }
}
基于HttpContext的细粒度Singleton实现

对于Web Form、ASP.NET、Web Service等Web类应用程序不适用ThreadStatic的方法,因为它们是在同一个IIS线程下分割的执行区域,客户端调用时传递的对象是在HttpContext中共享的。每个会话的本地全局区域不是线程,而是自己的HttpContext,因此我们需要作出少许的修改。

public class WebSingleton
{
    // 足够复杂的一个 key 值,用于和HttpContext 中的其他内容相区别
    private const string Key = "PracticalPattern.Generate.Singleton.WebSingleton";
    private WebSingleton() {}

    public static WebSingleton Instance
    {
        get
        {
            WebSingleton instance = (WebSingleton)HttpContext.Current.Items(Key);
            if (instance == null)
            {
                instance = new WebSingleton();
                HttpContext.Current.Items[Key] = instance;
            }

            return instance;
        }
    }

}

基于泛型的单件

有时候,项目中需要批量的产生很多Singleton类型。我们可以考虑在类的继承关系上提供一个模板式的实现方式,毕竟实现Singleton的方法相对固定,基本过程都是踏踏实实的生成一个实例。

public interface ISingleton { }

public abstract class GenericSingleton : ISingleton
    where T: ISingleton, new()
{
    protected static T instance = new T();

    public static T Instance { get { return instance; } }
}

public class SingletoneA : GenericSingleton, ISingleton { }
public class SingletoneB : GenericSingleton, ISingleton { }

需要注意的是,这种实现方式不是那么规范,在提供了子类快速实现的同时,在子类中对实例化的约束丢失了,用户也可以绕过Instance静态属性,直接实例化这些子类型。
Unit Test

[TestMethod]
public void Test_GenericSingleton()
{
    // 使用传统Singleton方式访问,保证唯一性。
    SingletoneA a1 = SingletoneA.Instance;
    SingletoneA a2 = SingletoneA.Instance;
    Assert.AreSame(a1, a2);

    // 绕过Instance静态属性,直接实例化,破坏了唯一性。
    SingletoneA a3 = new SingletoneA();
    Assert.AreNotSame(a1, a3);
}

这种不太规范的做法,也是有意义的:

  • 在项目中规范访问Singleton的属性,避免出现,GetInstance、Instance、Singleton等各种林林总总的方法。
  • 为客户程序提供一定的灵活度,在把唯一实例作为静态的服务访问点的同时,而言不排斥客户程序从实例层面操作Singleton类的方法。

Singleton-N扩展

Singleton模式明确定义了唯一的实例数量,它的根本目的是保证某些操作的一致性,比如更新计数器,更新整个应用记得某些全局性信息。但是在使用中,也可能会因为某些操作太过于耗时,而使Singleton成为整个应用的瓶颈。
那么我们可以考虑做一个扩展,让Singleton内部可以产生N个实例,大家分担工作量,这就不至于一个人累垮而且还拖累大家。我将它称为Singleton-N模式。

public enum Status
{
    Busy,   // 被占用
    Free
}

public interface IWorkItem
{
    Status Status { get; set; }
    void Deactive();    // 使用结束
}

public class WorkItemCollection
    where T : class, IWorkItem
{
    protected int _Max;
    protected IList items;

    public WorkItemCollection(int max)
    {
        this._Max = max;
        items = new List();
    }

    public virtual void Add(T item)
    {
        if (item == null) throw new ArgumentNullException("item");

        if (!CouldAddNewInstance) throw new OverflowException();

        item.Status = Status.Free;
        items.Add(item);
    }

    public virtual T GetWorkItem()
    {
        if (items == null || items.Count == 0) return null;

        // 寻找一个空闲的实例返回
        foreach (T item in items)
            if (item.Status == Status.Free)
            {
                item.Status = Status.Busy;
                return item;
            }

        return null;
    }

    public bool CouldAddNewInstance
    {
        get { return items.Count < _Max; }
    }        
}

public class SingletonN : IWorkItem
{
    private const int MaxInstance = 2;  // 定义Singleton-N的这个N

    private SingletonN()
    {
        Status = Status.Free;
    }

    public Status Status { get; set; }

    public void Deactive()
    {
        this.Status = Status.Free;
    }

    private static WorkItemCollection collection = 
        new WorkItemCollection(MaxInstance);

    public static SingletonN Instance
    {
        get
        {
            // 在基本实现框架不变的情况下,引入集合实现Singleton-N的多个实例管理。
            SingletonN instance = collection.GetWorkItem();

            if (instance == null)
            {
                if (!collection.CouldAddNewInstance) return null;
                else
                {
                    instance = new SingletonN();
                    collection.Add(instance);
                }
            }

            instance.Status = Status.Busy;  // 激活使用
            return instance;
        }
    }
}

Unit Test

[TestMethod]
public void Test_SingletonN()
{
    SingletonN s1 = SingletonN.Instance;
    SingletonN s2 = SingletonN.Instance;
    SingletonN s3 = SingletonN.Instance;

    Assert.IsNotNull(s1);
    Assert.IsNotNull(s2);
    Assert.IsNull(s3);  // 超出容量,未获得实例返回。

    // 两个不同的实例
    Assert.AreNotSame(s1, s2);

    s1.Deactive();
    s3 = SingletonN.Instance;
    Assert.IsNotNull(s3);   // 有实例被释放后,获得实例

    // 获得的实例为之前创建的实例
    Assert.AreSame(s1, s3);            
}

从某种程度上来说,Singleton-N是ObjectPool模式的一个预备模式,它除了完成创建型模式最一般的new()外,还要负责检查和数量控制。
以上的代码中,没有加锁机制,也没有做线程完全的检查,仅仅实现了一个Singleton-N模式的功能性部分。
实际上,应用中如果出现需要使用Singleton-N的情况,往往和设计的类型间职责划分有一定关系。如果方法间没有明显的耦合情况,那么完全可以把算法提取为另外一个类,它本身就不需要Singleton;如果方法很难拆解,或者不能暴露内部结果,对共享资源的调用竞争比较激烈,那么Singleton-N就是大展身手之时。

粗粒度的单件——跨进程的Singleton

以上素有的讨论都是限制在一个线程内的,但随着分布式应用的发展,很多关键应用都需要通过Cluster实现应用,那么麻烦就大了。因为如果应用运行在不同的服务器上,甚至跨越网络,无论用上面的那种实现方式都很难控制那个 instance == null的判断。
这个作为一个思考题,供大家考虑。我将在后续的文章中提供解决方案。

你可能感兴趣的:(设计模式,c#)