僅將此篇文章獻給我的摯友、導師,Charles,此篇文章參考了大部分他的知識和文章,原文:程湘之間
Amy(PO):
As a 業務副總
I want 在明天的新產品線上預購活動,讓預購客戶取得一個唯一的預購號碼,而且要在產品發表會螢幕上打上線上預購的數目。
So that 由這個成功的產品發表會來增加產品曝光率
Hachi:
「在產品發表會螢幕上打上線上預購的數目」? 讓我想到購物節電商即時顯示的即時交易數據!
JB:
你說的沒錯! 這次就是這樣搞!
不過我比較擔心如果預購的人很零星,那個螢幕上的預購數字動很慢...或跟本不動...不就太瞎了...
Lily:
別替他們擔心! 業務部的人已經評估一個預購量,讓我們在預購開始時,直接逐筆累加在螢幕上的預購數! 讓現場看起來幾秒之內我們就收到了幾萬筆預購!! 然後螢幕放個綵帶的動畫...副總上台致詞轉移焦點...結束完美的發表會!
Hachi:
所以我們寫個迴圈,每秒累加一個亂數就算完成這個需求了嗎!
Lily:
哈哈! 你學的很快! 不過我們重點是在於真正線上客戶的預購! 畢竟他們收到的預購號碼不能重複!
所以我們加上單例模式(Singleton)來滿足這個需求!
確保類別只有一個實例(instance),並提供所有對象訪問這個實例的方法。(WIKI)
在C#中,我們可以將 Singleton分成以下幾種:
在實作Singleton類別前,我們先建立一個提供預購號碼的父類別:
public class NumberProvider
{
protected int Counter = 0;
public int GetNumber()
{
Counter++;
return Counter;
}
}
接著我們直接讓Singleton類別繼承NumberProvider
以在主程式可以使用GetNumber()
。
讓我們可以專注在如何確保Singleton類別永遠只有產生一個實例。
public sealed class NonThreadSafeSingleton: NumberProvider
{
private static NonThreadSafeSingleton INSTANCE = null;
public static NonThreadSafeSingleton Instance
{
get
{
if (INSTANCE == null)
INSTANCE = new NonThreadSafeSingleton();
return INSTANCE;
}
}
}
在多執行緒環境,有可能在取得實體時:NonThreadSafeSingleton.Instance
造成在各執行緒同時判斷INSTANCE == null
成立,而重複建立INSTANCE
的情況。
例如在單元測試程式模擬同時有十位客戶(10 Threads)作線上預購,結果使用Non thread-safe Singleton造成其中五位客戶產生了重複的預購號碼: 1 和 2 。
(19)陳 先生預購2組,預購號碼為: 1,2
(21)施 先生預購2組,預購號碼為: 1,2
(20)謝 先生預購2組,預購號碼為: 1,2
(22)林 先生預購2組,預購號碼為: 1,2
(8)林 先生預購2組,預購號碼為: 1,2
(8)李 先生預購2組,預購號碼為: 11,12
....
PS. 以上輸出格式為 (執行緒ID) XXX預購N組: 預購號碼
我們改採用lock(object)
來確保同時只有一條thread可以建立instance。
public sealed class DoubleCheckSingleton: NumberProvider
{
private static DoubleCheckSingleton INSTANCE = null;
static readonly object padlock = new object(); //用來LOCK建立instance的程序。
public static DoubleCheckSingleton Instance
{
get
{
if (INSTANCE == null)
{
lock (padlock) //lock此區段程式碼,讓其它thread無法進入。
{
if (INSTANCE == null)
{
INSTANCE = new DoubleCheckSingleton();
}
}
}
return INSTANCE;
}
}
}
比較簡潔的Singleton寫法,在宣告INSTANCE
時即建立實體,所以加載此類別時實體會立即被建立。
public sealed class EagerSingleton: NumberProvider
{
private static EagerSingleton INSTANCE = new EagerSingleton();
public static EagerSingleton Instance
{
get
{
return INSTANCE;
}
}
}
不同於Eager Singleton,Lazy Singleton只會在真正使用到實體時才建立。
public sealed class LazySingleton : NumberProvider
{
public static LazySingleton Instance
{
get
{
return InnerClass.instance;
}
}
class InnerClass
{
static InnerClass()
{
}
internal static readonly LazySingleton instance = new LazySingleton();
}
}
以上除了Non thread-safe Singleton,其他皆可確保在多執行緒環境下建立單例實體。
但是要注意在操作這些單例類別裡面的變數時,也需要確保它的存取是Thread safe的。
所以我們更新NumberProvider
裡面的GetNumber()
方法如下:
public class NumberProvider
{
private static readonly object numberBlock = new object();
protected int Counter = 0;
public int GetNumber()
{
lock (numberBlock)
{
Counter++;
return Counter;
}
}
}
這樣就可以確保在多個客戶同時線上預購時,不會取到相同的號碼。
單元測試程式碼請參考這裡。
在Python因為沒有private修飾詞,無法在類別裡建立私有的變數來作為單例實體。
比較直覺的方式是採用module的方式來作為Singleton模式。
或是利用以下程式碼建立一個單例類別NumberProvider
。
def singleton(cls):
obj = cls()
# Always return the same object
cls.__new__ = staticmethod(lambda cls: obj)
# Disable __init__
try:
del cls.__init__
except AttributeError:
pass
return cls
@singleton
class NumberProvider():
counter =0
def getNumber(self):
self.counter+=1
return self.counter
在多執行緒的測試程式碼:
class GetNumberThread(threading.Thread):
def __init__(self):
super().__init__()
def run(self):
for i in range(0, 30):
singleton = NumberProvider()
number = singleton.getNumber()
print(number)
if(NumberProvider()==NumberProvider()):
print('Correct!')
threads = []
for i in range(0,10):
thread = GetNumberThread()
threads.append(thread)
thread.start()
for thread in threads:
thread.join()