什麼是 Singleton?(以 C# 為例)

前情提要(?

前幾天有噗友在河道上面[1]討論 Singleton 的實做,剛好有參與到討論所以就順便筆記下來,下面舉例的程式碼都以 C# 作為標準(沒為什麼,就只是因為這樣可以複製貼上(懶
(而且我還拖稿數月哈哈哈哈哈哈哈哈")

正文

Singleton(單例)是一種軟體設計模式,這裡引用 wikipedia[2] 的片段:

單例模式,是一種常用的軟體設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行為。

實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法;當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。

單例模式在多執行緒的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個執行緒同時調用創建方法,那麼它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。解決這個問題的辦法是為指示類是否已經實例化的變量提供一個互斥鎖(雖然這樣會降低效率)。

簡單的來說,Singleton 的目標是讓某個希望被大家共同使用的類別在同一個時間(通常是指同個執行時期)只會有一個實例存在。

其實這種設計模式非常的簡單,通常會搭配 static member 來儲存實例,只是有些需要注意的地方要小心,那麼我們先從幾個常見的錯誤來示範,然後再慢慢推進完美(?

using System;

class SingletonA {
    private static SingletonA instance = null;

    public static SingletonA Instance {
        get {
            return instance ?? new SingletonA();
        }
    }

    private SingletonA() {
        instance = this;
    }
}

整個程式的流程就會如下圖所示,有任意程式碼需要使用到 SingletonA 的時候,就會呼叫 SingletonA.Instance 來取得唯一的 SingletonA 實體。若 instance 尚未被初始化時就會 new 一個新的實體並存放在 instance 這個靜態變數中:

sequenceDiagram
    A->>class SingletonA: get SingletonA.Instance
    Note over class SingletonA, SingletonA instance: if ( instance == null )
    class SingletonA->>SingletonA instance: new
    Note over SingletonA instance: SingletonA.instance = this
    SingletonA instance-->>class SingletonA: 
    class SingletonA->A: return instance

改良

上面的流程聽起來好像不錯,但是有人可能會覺得讓建構子來將自己賦值到靜態的變數感覺不是很棒,應該讓 static method(get Instance)來管理 static member(instance)才對,於是我們可以做以下的小修改:

using System;

class sealed SingletonB {
    private static SingletonB instance = null;

    public static SingletonB Instance {
        get {
            if (instance == null) {
                instance = new SingletonB();
            }
            return instance;
            // 或寫成下列寫法
            // return instance ?? (instance = new SingletonB());
        }
    }

    private SingletonB() { }
}
sequenceDiagram
    A->>class SingletonB: get SingletonB.Instance
    Note over class SingletonB, SingletonB instance: if ( instance == null )
    class SingletonB->>SingletonB instance: new
    SingletonB instance-->>class SingletonB: 
    Note over class SingletonB: instance = SingletonB instance
    class SingletonB->>A: return instance

另外我們使用了 sealedSingletonB 無法被繼承,因為 private 仍然可以讓 nested class 存取,如果此時有另外的型別在 SingletonB 底下的話,他就有機會可以存取建構子(透過繼承)。

多執行緒?

雖然 static member 讓 static method 管理看起來更加整齊了但是我們還是沒有辦法避免 concurrency 的問題,當程式有多個執行緒在運作的時候,有可能會遇到同時間有兩個執行緒都想要存取這個 Singleton,但此時才初始化的話可能會遇到如下圖所示的問題:

sequenceDiagram
    participant A
    participant instanceA
    participant class Singleton
    participant instanceB
    participant B
    A->>class Singleton: get Singleton.Instance
    B->>class Singleton: get Singleton.Instance
    Note over A,class Singleton: if ( instance == null )
    Note over class Singleton,B: if ( instance == null )
    class Singleton->>instanceA: new
    instanceA-->>class Singleton: 
    class Singleton->>instanceB: new
    instanceB-->>class Singleton: 
    Note over class Singleton: instance = instanceA
    class Singleton->>A: return instance
    Note over class Singleton: instance = instanceB
    class Singleton->>B: return instance

結果兩個執行緒 A, B 分別各自取得的 Singleton 並不是同一個,這時候就需要針對多執行緒作一些修改,在這裡有兩派的做法各自有各自的好壞,大家可以自己決定使用的方式。

Static Initialization

這個做法是透過直接在宣告 static member 時就賦予一個初始化後的 instance 作為其值,注意這個做法在 Design Pattern[3] 中並不推崇,那是因為 static initialization 的過程在 C++ 中仍有一些未定義的部分。但如果你使用的語言允許的話,這個做法其實是不錯的:

using System;

class sealed SingletonC {
    private static readonly SingletonC instance = new SingletonC();

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

    private SingletonC() { }
}

在這裡我們使用 readonly 修飾詞來宣告 instance 變數只能透過 static initialization (或建構子)來賦值。

另外 C# 中 static initialization 的時機是「當型別內的任何成員被提到的時候」,也就是說如果沒有任何的程式呼叫過 SingletonC,那他永遠不會被初始化;反之,當第一次呼叫 SingletonC.Instance 的時候才會進行 SingletonC.instance 的初始化,這部份不同的語言可能會有不同的實做,大家可以考慮看看要不要使用。

使用鎖

在不能使用 Static initialization 的情況下,我們可以透過鎖(lock)來控制一次只有一個執行緒可以對 instance 進行初始化:

using System;

class sealed SingletonD {
    private static volatile SingletonD instance;
    private static object syncRoot = new Object();

    public static SingletonD Instance {
        get {
            if (instance == null) {
                lock (syncRoot) {
                    if (instance == null) {
                        instance = new SingletonD();
                    }
                }
            }

            return instance;
        }
    }

    private SingletonD() { }
}

這裡檢查了兩次 instance 是否為 null,因為有可能在等待鎖的時候就已經有其他執行緒對 instance 進行初始化了,所以在鎖的內部也需要再檢查一次。

另外,我們加上了 volatile 修飾詞來避免編譯器最佳化導致真正初始化 instance 前就被其他執行緒取用。值得一提的是,這裡我們添加了另外的靜態物件 syncRoot 來提供鎖的依據,當使用其他語言實做的時候可以替換成其他方便使用的全域物件即可(例如 Java 中就可以使用 .getClass() 來當作鎖)。

更多資訊可以參考 MSDN C# 實做 Singleton 的說明[4]


  1. https://www.plurk.com/p/m6keb4 ↩︎

  2. 單例模式 - 維基百科,自由的百科全書 ↩︎

  3. Design Patterns: Elements of Reusable Object-Oriented Software ↩︎

  4. Implementing Singleton in C# ↩︎

因主題更新,留言功能暫時停用中。