Symbian中singleton的實現(多線程)

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 的成員用來向列
表中增加靜態實例,但是沒找到從列表中刪除實例的成員函數。夠用了,呵呵

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章