Symbian中singleton的實現(多線程)
EKA2中可以用WSD實現,不過內存開銷很大。EKA1中用Tls實現,其中有些技巧。
在Symbian開發者網站的這個新欄目成立之初, Jo Stichbury開放診所並提供與Symbian C++相關的建議。
在本月的診所文章裏,她研究了在Symbian OS DLL中使用可修改全局數據(也稱爲可寫靜態數據)的侷限性。
新的代碼診所文章將於每月的第一個星期五發表。你有Symbian C++方面的問題?請將它發送至位於[email protected]的代碼醫生。
如果問題被接受,你會獲得Symbian出版社出版的 Symbian OS軟件開發(第2版)。
親愛的代碼醫生
我正在將我的代碼移植到Sybian OS,但是我聽說在Symbian OS中全局數據的使用有所限制,這是真的嗎?如果我嘗試使用全局 數據會發生什麼呢?這會妨礙我移植採用單例設計模式的代碼嗎?
Anxious About Singleton
親愛的Anxious About Singleton,
首先,不要慌!是的,在Symbian OS中有一些情況是不建議使用全局數據,但是這種限制只存在於DLL中——如果你編寫的是EXE,那麼程序是不會受到影響的。
我們這裏討論的“可修改全局數據”指的是任何非常量的全局域變量或任何非常量的函數域靜態變量。例如:
TBufC<20> fileName; // 可修改全局數據
void SetFileName()
{
static TInt iCount; // 靜態變量
...
}
爲了簡略表示,Symbian通常將這類變量稱爲WSD,代表可寫靜態數據。
壞消息
在包含最初內核結構,EKA1的手機上如果要運行DLL,那麼該DLL中是不允許使用WSD的。這意味着在Symbian OS v8.1a以前(對應於S60和UIQ參見下面的表格)的DLL中是不存在WSD的。
隨着新的內核結構(EKA2,在Symbian OS v8.1b和Symbian OS v9中採用)的出現,這種局面得到改善,在DLL中使用WSD成爲可能。然而,由於WSD在內存是使用方面開銷很大,在編寫會被很多進程載入的共享DLL時,不建議使用WSD。這是因爲對於每個載入DLL的進程,DLL中的WSD通常需要消耗4KB的內存。
當進程載入第一個包含WSD的DLL時,它創建一個單獨的虛擬內存區域來保存該WSD。一個虛擬內存區域最小爲4KB,同時消耗的內存與現實需要的靜態數據量無關。任何供WSD使用但是沒有被使用的內存都浪費了(然而,如果隨後的DLL載入該進程並且也使用WSD,那麼相同的虛擬內存區域可供使用,而不用爲每個使用WSD的DLL都分割4KB的虛擬內存區域)。
既然內存是針對進程而言,那麼潛在的內存浪費量爲:
(4KB – WSD 字節) × 客戶端進程數舉例來說,如果一個DLL被4個進程使用,那麼潛在的“隱形”內存開銷爲16KB(減去WSD自身佔用的內存)。I
此外,運行於Windows PC上的Symbian OS仿真器並不完全支持使用WSD的DLL。在單獨的正在運行的進程中,仿真器只能載入一個使用WSD的DLL(原因是Symbian OS仿真器在Windows上層的實現方式)。如果在仿真器中有第二個進程試圖載入相同的DLL,該操作會失敗並返回錯誤代碼KErrNotSupported。Symbian OS v9.4引入該問題的解決方法。更多信息可以從 [R1]獲得。
在很多場合,在DLL中使用WSD獲得的好處要大於其缺點(比如,當移植大量使用WSD的代碼時,以及使用那些只會被一個進程載入一次的DLL)。作爲一個第三方開發者,你可能覺得WSD的內存開銷是可接受的,比如,如果你創建一個只被一個應用程序載入一次的DLL。但是注意,當前支持的GCC-E編譯器版本有個缺陷,即使用靜態數據的DLL可能會在載入時導致嚴重錯誤。這個問題,以及其解決方法,在Symbian開發者網站的FAQ1574中得到討論。
然而,如果你的工作是設備創造(比如,創造用於Symbian OS,某一個UI平臺,或手機設備上的共享DLL),那麼折中標準就不同了。你的DLL可能被許多進程使用,帶來的內存開銷和限制會使得WSD的使用缺乏理由。
好消息
如果你的工作針對Symbian OS v9而不是Symbian OS的早期版本,那麼你受到WSD限制的影響就會較小。在Symbian OS v9之前,應用程序編譯爲DLL,開發者如果需要移植使用WSD的應用程序,就不得不尋找解決方法。在Symbian OS v9中,應用程序框架結構的改變意味着現在所有的應用程序是EXE而不是DLL。在EXE中總是允許使用可寫靜態數據,所以如果需要,應用程序現在可以使用WSD。
下表總結不同Symbian UI平臺中DLL和應用程序對WSD的支持情況:
UI 平臺 Symbian OS 版本 Symbian OS 內核 運行於該設備上的DLL支持使用WSD? 應用程序允許使用WSD?
UIQ 2.x
S60 1st 和 2nd 版本 v7.0 (UIQ)
v7.0s, v8.0a, v8.1a (S60) EKA1 不支持 不允許
UIQ 3 S60 3rd 版本 v9 EKA2 支持,但是不建議在不理解Windows仿真器限制和內存開銷要求的前提下使用 允許——應用程序是EXE
用法
至此,我已經回答了你關於在Symbian OS DLL中可修改全局數據所受限制的問題,需要更多信息請參見[R1]。讓我們進一步看看使用WSD會發生什麼。以下討論的前提假設是你的工作只針對Symbian OS v9。
首先,有時候你會發現自己不自覺地使用WSD。一些Symbian OS類具有非平凡的構造函數,這意味着其對象必須在運行時構造。你可能認爲自己沒有使用WSD,但是由於一個對象直到其構造函數執行才被實例化,該對象被認爲是可修改的,而不是常量。這裏是一些示例:
static const TPoint KGlobalStartingPoint(50, 50);
static const TChar KExclamation('!');
static const TRgb KDefaultColour(0, 0, 0);
在大部分的Symbian OS版本中,如果你試圖編譯使用了WSD的DLL,不論你是否有意這樣做,你在針對手機硬件編譯DLL時都會遇到錯誤。該錯誤與如下類似:
錯誤: Dll 'TASKMANAGER[1000C001].DLL' 含有未初始化的數據
你會發現使用WSD的DLL總是針對Windows仿真器編譯。只有當代碼針對手機硬件編譯時,使用WSD纔會標記爲錯誤。參考文獻[R2]說明了無意使用WSD時如何對其進行追蹤。如果你確定要使用WSD,你可以通過在MMP文件中加入關鍵字EPOCALLOWDLLDATA來實現。
然而,值得注意的是有些Symbian OS版本(例如,Symbian OS v9.3,發現於S60 3rd版本FP2)無論MMP文件中是否存在 EPOCALLOWDLLDATA,都不將DLL中的WSD標記爲錯誤。這是由於默認情況下特殊的編譯標誌位被開啓。
不存在WSD的Singleton實現
開發者從其它平臺移植代碼通常遇到的問題是Symbian OS DLL中對WSD的限制會影響經典單例設計模式的實現。單例設計模式無疑是經典設計模式書籍[R3]中最流行的設計模式。它是一種最簡單的設計模式,只調用一個類來提供全局指針訪問某一實例,該示例自己完成實例化。
在C++中使用WSD的單例設計模式的經典實現如下所示:
class Singleton
{
public:
static Singleton* Instance();
... // Singleton提供的操作
private: // 爲了表示清楚,這些函數沒有實現
Singleton();
~Singleton();
private: // 靜態Singleton成員變量
static Singleton* pInstance_;
};
/*static*/ Singleton* Singleton::pInstance_ = NULL;
/*static*/ Singleton* Singleton::Instance()
{
if (!pInstance_)
pInstance_ = new Singleton();
return (pInstance_);
}
如前所述,在Symbian OS v9的DLL中實現Singleton是可能的,這可以通過在MMP文件中顯式使能WSD來實現。你可以按照上述代碼,使用Symbian C++命名規範和處理實例化過程中異常退出的標準用語來定義一個Singleton類。
然而,如果你希望避免潛在的額外內存消耗和仿真器測試限制,還有一種可供選擇的機制可用:即線程本地存儲(TLS)。TLS可用於在所有Symbian OS版本的DLL中實現Singleton(如果需要也可用於EXE中)。TLS是單獨的線程存儲區域,大小爲一個機器字(在Symbian OS v9中爲32比特)。一個指向本地的,堆存儲的Singleton對象的指針保存於TLS區域,一旦需要訪問該Singleton對象,就可以使用TLS中的該指針。
訪問TLS的操作位於類Dll中,該類位於e32std.h:
static TInt SetTls(TAny* aPtr); // 設置TLS數據
static TAny* Tls(); // 獲得保存於TLS中的指針
static void FreeTls(); // 清除TLS數據
我們馬上來看一下使用TLS的Singleton的典型實現。但是首先,這麼做有什麼缺點呢?簡而言之,是運行時性能的降低。從TLS中獲取數據比直接訪問慢大約30倍,這是因爲該查找過程通過執行調用轉移至內核執行程序(更多信息參見[R4])。
此外,每個線程中TLS只有一個空位。如果線程中TLS移爲他用,那麼所有的TLS數據必須放置一個單獨的類中,並且通過TLS指針訪問。這可能很難維持(儘管對於應用程序開發者來說有解決方案,正如隨後部分Singleton for Application Developers所描述的)。
在Symbian OS中,Singleton的實現必須考慮實例化失敗的可能性(例如,沒有足夠的內存分配給Singleton實例時,就會發生異常退出)。一種可選方法是提供兩個分離的函數:
一個工廠函數,NewL(),用於實例化Singleton實例並將其保存在TLS空位中。這可能失敗,所以調用者必須處理所有可能發生的異常退出。
一個分離的不會發生異常退出的函數,Instance(),該函數用於在Singleton實例化之後通過從TLS中獲取對象位置來訪問該實例。
該方法爲調用者提供更多的靈活性。爲了實例化Singleton實例,只需要調用可能發生失敗的函數。在實例化過程中,Singleton保證以引用形式返回,所以不要求指針檢查或安全退出代碼。
class CSingleton : public CBase
{
public:
// 創建Singleton實例
IMPORT_C static void NewL();
// 訪問Singleton實例
IMPORT_C static CSingleton& Instance();
private: // 爲了表示清楚,這些函數沒有實現
CSingleton();
~CSingleton();
void ConstructL();
};
EXPORT_C /*static*/ void CSingleton::NewL()
{
if (!Dll::Tls()) // 不存在Singleton實例。創建一個。
{
CSingleton* singleton = new(ELeave) CSingleton();
CleanupStack::PushL(singleton);
singleton->ConstructL();
User::LeaveIfError( Dll::SetTls(static_cast<TAny*>(singleton)) );
CleanupStack::Pop(singleton);
}
}
EXPORT_C /*static*/ CSingleton& CSingleton::Instance()
{
CSingleton* singleton = static_cast<CSingleton*>(Dll::Tls());
ASSERT(singleton); // 調試編譯下出現嚴重錯誤
return (*singleton);
}
爲了更加符合經典模式,Singleton的一部分實現更趨向於提供單獨的可能發生異常退出的訪問函數InstanceL()。
class CSingleton : public CBase
{
public:
// 訪問/創建Singleton實例
IMPORT_C static CSingleton& InstanceL();
private: // 爲了表示清楚,這些函數沒有實現
CSingleton();
~CSingleton();
void ConstructL();
};
EXPORT_C /*static*/ CSingleton& CSingleton::InstanceL()
{
CSingleton* singleton = static_cast<CSingleton*>(Dll::Tls());
if (!singleton) // 不存在Singleton實例。創建一個。
{
singleton = new(ELeave) CSingleton();
CleanupStack::PushL(singleton);
singleton->ConstructL();
User::LeaveIfError( Dll::SetTls(static_cast<TAny*>(singleton)) );
CleanupStack::Pop(singleton);
}
return (*singleton);
}
該方法的優點在於實現部分可以定製,例如,執行引用計數。然而,每次調用InstanceL()都必須考慮異常退出的可能性,這爲調用者增加了負擔,並且由於大量使用TRAP活在更復雜的代碼中使用清理棧而潛在地降低效率。
在Symbian OS v9之前,應用程序都是DLL,都無法使用WSD。Singleton基於TLS的實現被認爲是在經典模式下使用WSD的一種直接的替代方式。爲了保證該過程的簡易型,Symbian OS爲應用程序開發者提供額外的機制,這在下面的Singleton:應用程序開發者必讀部分得到討論。
從Symbian OS v9開始,應用程序是EXE而不是DLL,因此在應用程序代碼中使用WSD不再受限制。
多線程代碼
注意,以上所示的TLS實現正常工作的前提是隻有一個線程需要訪問Singleton。正如“線程本地存儲”這個名字本身的含義,TLS使用的存儲字相對於線程來說是本地的;一個進程中的每一個線程都有自己的存儲位置。在多線程環境中如果要使用TLS訪問一個Singleton對象,那麼引用該Singleton對象位置的指針必須傳遞給每一個線程,並且保存在TLS空位中。這可以在每個新線程創建的時候使用RThread::Create()的適當參數實現。如果該操作沒有實現,當新線程調用Dll::Tls()獲得Singleton位置時,該函數會返回一個NULL指針。
因此Singleton的創建必須由父線程來管理。父線程在其他線程存在之前創建Singleton,並且在其它線程創建的時候將Singleton的位置進行傳遞。這些線程必須使用Dll:SetTls()來保存Singleton的位置。
讓我們來看看下面的代碼,它對以上機制的工作方式進行說明。首先,CSingleton類輸出兩個附加函數,通過調用代碼,這兩個函數可以用來從主線程獲取Singleton實例指針(SingletonPtr()),然後將其在創建的線程中進行設置(InitSingleton())。
class CSingleton : public CBase
{
public:
IMPORT_C static void NewL();
IMPORT_C static CSingleton& Instance();
// 傳遞Singleton位置至新線程
IMPORT_C static TAny* SingletonPtr();
// 在新線程中初始化對Singleton的訪問
IMPORT_C static TInt InitSingleton(TAny* aLocation);
private:
CSingleton();
~CSingleton();
void ConstructL();
};
EXPORT_C TAny* CSingleton::SingletonPtr()
{
return (Dll::Tls());
}
EXPORT_C TInt CSingleton::InitSingleton(TAny* aLocation)
{
return (Dll::SetTls(aLocation));
}
// 爲了表示清楚,忽略其它函數
// NewL()和Instance()可參見前述代碼
爲了解釋清楚,這裏附上進程主線程的基本代碼,該主線程創建一個Singleton實例,然後創建此線程,並把Singleton的位置傳遞給它。
// 主(父)線程創建Singleton
CSingleton::NewL();
// 創建次線程
RThread childThread;
User::LeaveIfError(childThread.Create(_L("childThread"),
ChildThreadEntryPoint, KDefaultStackSize,
KMinHeapSize, KMaxHeapSize,
CSingleton::SingletonPtr()));
CleanupClosePushL(childThread);
// 恢復thread1,等等...
注意,線程創建函數以CSingleton::SingletonPtr()的返回值作爲參數值。該參數值必須傳入位於子線程進入點函數的CSingleton::InitSingleton()中。
TInt ChildThreadEntryPoint(TAny* aParam)
{// 在TLS中保存Singleton的位置
if ( CSingleton::InitSingleton(aParam)==KErrNone )
{// 成功,正常運行
...
}
return (0);
}
Singleton:應用程序開發者必讀
Symbian OS提供類CCoeStatic來幫助應用程序開發者將其它平臺中使用WSD的應用程序代碼進行移植。在Symbian OS的早期(Symbian OS v9之前),應用程序都是DLL並且不允許使用WSD,該類是非常有用的。現在,在Symbian OS v9中,已經沒有必要使用這個類,因爲應用程序是EXE並且可以使用WSD。然而,如果你決定使用TLS,移植工作也很簡單,並且允許不止一個DLL線程使用一個TLS空位。
該方法很直接——只需要從CCoeStatic繼承你的Singleton類。例如:
class CAppSingleton : public CCoeStatic
{
public:
static CAppSingleton& InstanceL();
static CAppSingleton& InstanceL(CCoeEnv* aCoeEnv);
private:
CAppSingleton();
~CAppSingleton();
};
該類的實現必須將自身與UID關聯起來,以允許Singleton實例“註冊”到應用程序框架中(類CCoeEnv)。當Singleton對象實例化後,CCoeStatic基類構造函數將該對象添加至CCoeEnv保存的Singleton列表中。在內部,CCoeEnv使用TLS來保存每個註冊Singleton對象的指針(使用包含指向CCoeStatic派生對象指針的雙向鏈表)。因此,對於類CAppSingleton:
const TUid KUidMySingleton = {0x10204232};
// "register"singleton
CAppSingleton::CAppSingleton()
: CCoeStatic(KUidMySingleton, CCoeStatic::EThread)
{}
// 使用CCoeStatic::Static()訪問Singleton
CAppSingleton& CAppSingleton::InstanceL()
{
CAppSingleton* singleton =
static_cast<CAppSingleton*>(CCoeStatic::Static(KUidMySingleton));
if (!singleton)
{// 忽略二階段構造
singleton = new(ELeave) CAppSingleton();
}
return (*singleton);
}
// 使用CCoeStatic::FindStatic()訪問Singleton
CAppSingleton& CAppSingleton::InstanceL(CCoeEnv* aCoeEnv)
{
CAppSingleton* singleton = static_cast<CAppSingleton*>
(aCoeEnv->FindStatic(KUidMySingleton));
if (!singleton)
{// 忽略二階段構造
singleton = new(ELeave) CAppSingleton();
}
return (*singleton);
}
一旦CAppSingleton類完成實例化,就可以通過InstanceL()函數訪問它,也可以直接調用CCoeEnv::Static()或CCoeEnv::FindStatic()進行訪問。注意前者是靜態函數,所以可以在CCoeEnv指針不可用的應用程序中使用。
// 來自coemain.h
static CCoeStatic* Static(TUid aUid);
CCoeStatic* FindStatic(TUid aUid);
這些函數遍歷雙向鏈表,試圖將CCoeStatic派生對象和應用程序UID進行匹配。CCoeStatic只能被運行於應用程序框架內部的代碼使用,例如控件環境(CONE)。
Singleton清理
本文中討論的所有實現都聲明Singleton類的析構函數爲私有函數,並且返回的是Singleton引用而不是指針。這是因爲,如果析構函數是公共的,並且返回的是Singleton實例的指針,那麼它可能被其它調用者無意銷燬,使得Instance()函數處理已經刪除示例的“虛引用”。給出的實現防止了這種情況的發生,並且讓Singleton類負責Singleton實例的創建、所有權,以及最終的清理。
清理的通常方法是使用標準C程序庫提供的atexit函數與清理函數進行註冊,這些清理函數在進程終結的時候被顯式調用。清理函數可以是Singleton類的成員函數,簡單地刪除Singleton實例。更多詳情請參見 [R5]。
然而,你也許希望在進程終結之前銷燬Singleton(例如,如果該對象不再需要,就可以釋放其佔有的內存空間)。在這種情況下,你必須考慮引用計數,以避免由於過早刪除造成的“虛引用”,正如[R5]討論的一樣。
更多信息
本次討論部分選自即將出版的Symbian出版社書籍,Common Design Patterns on Symbian OS中的單例模式說明部分。需要關於該書的更多信息,請查看維基主頁的插入URL/FURL。
參考書目
[R1] Symbian OS對DLL中可寫靜態數據的支持,Hamish Willee,2008年1月。
[R2] 如何發現WSD的疏忽使用?
[R3] 設計模式:可複用面向對象軟件的基礎,Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides,Addison Wesley,1995年。
[R4] Symbian OS內核結構,Jane Sales et al,John Wiley & Sons,2005年。
[R5] C++設計新思維:泛型編程與設計模式之應用,Andrei Alexandrescu,Addison-Wesley Professional 2001年。
貢獻者
我要感謝Adrian Issott,Mark Jacobs,Antti Juustila,Hamish Willee和Tim Williams,他們爲本文的很多方面提供反饋意見。
當dll想爲每一個線程創建單獨的對象時候就會用到tls。
dll創建的對象是在線程堆上的(當然,也可以是共享堆)。
tls一定得有一張表,這張表維護了線程對象關係。內容是線程ID,dll名,以及一個指針,指向對象。
在symbian中這張表存在於DThread中,所以就不需要線程ID了。
在dll中調用Dll::SetTls會調用User::SetTls,User::SetTls會找到當前線程,然後找到dll對應的表項,然後Add或Update表項。
要使用數據的時候也是到這張表裏面去找。因爲這張表是DThread維護的,所以不同線程的表是不一樣的,得到的對象自然也不同(這就是爲什麼叫thread local了)。
注:DThread是RThread成對出現的。DThread位於kernel中。
所以說要用tls共享對象(實現singleton)的時候,需要把在主線程中創建對象的指針告訴所有別的線程,別的線程更新DThread的tls項,這樣所有的thread的tls項才能都指向同一個對象。
tls效率肯定不高,因爲有好幾次的間接指針。而且它是存於DThread裏面的,那麼存取它需要一個軟中斷,讓cpu進入特權層,訪問DThread。
那爲什麼還要用tls呢。主要是wsd比較浪費空間。
每一個進程都會創建一個static data chunk(如果需要的話),用來hold dll中的靜態數據。
如果多個進程都用這個dll,代碼段是共享的,但是每一個進程都要有這麼一個chunk爲dll hold數據。
我不確定它在我們調用之前是否會被框架代碼調用static()並創建對象。
但既然框架提供了,就肯定會確保它的可靠性。也就是說,任何Singleton如果依託CCoeStatic創建,我們就可以保證CCoeStatic的唯一實例對象創建在用戶Singleton實體之前。
那麼,我們就可以放心將Singleton實體交予CCoeStatic管理。在CCoeStatic創建之後我們創建自己的Singleton,在我們釋放所有的singlton之後,再釋放CCoeStatic實例對象。
Singleton 實體如果能找到一個安全可靠的依託,這樣就可以擺脫依賴編譯器來產生和destroy 實體次序,這個問題就找到了一個比較好的解決方案。
比如:
CCoeStatic 的私有成員,非靜態。
private:
Test* m_pInstane; // 構造函數時賦值0
static CCoeStatic::TestInstance()
{
if(!CCoeStatic::static()->m_pInstane)
{
CCoeStatic::static().lock(); // 加鎖
if(!CCoeStatic::static()->m_pInstane)
{
CCoeStatic::static()->m_pInstane = new Test;
AddSingltonList(CCoeStatic::static()->m_pInstane); // 加入雙向鏈表
}
}
return CCoeStatic::static()->m_pInstane;
}
這裏面加入了比較古怪的double check,來提高效率。同時也能確保CCoeStatic實體能在所有的實體之前創建,加到雙向鏈表後也保存了單例的創建順序。我們在CCoeStatic,提供的析夠函數,或者爲其提供一個清理函數中,保證釋放內存操作是反序的。
CCoeStatic 是在沒有 WSD 的時候使用 TLS 實現的。派生類的實例指針保存在 TLS 裏面
(這些數據應該是由 CCoeEnv 維護),並不需要 m_pInstane 這樣的東西。實例的創建和
內部的數據安全的確要派生類自己負責。整體上是一個比較弱的框架。
我覺得派生類在實例創建的時候怎麼保證線程安全不是很大的問題。因爲在使用這個 TLS 版
的單實例框架的時候,需要在編碼之前就確定實例由哪個線程創建,有哪些線程可能用這個
東西,以及在什麼時候可以銷燬 —— 畢竟在不同線程之間共享這個實例的時候,創建實例的
線程必須顯式要其它線程設置它的 TLS。Vincent 說的”父線程創建,然後分發給子線程“的方
法在沒有 WSD 的時候應該足夠用了。
翻了一下 ccoemain.h,發現一個有趣的東西:CCoeEnv只有一個 AddStatic 的成員用來向列
表中增加靜態實例,但是沒找到從列表中刪除實例的成員函數。夠用了,呵呵