Symbian中singleton的实现(多线程)
EKA2中可以用WSD实现,不过内存开销很大。EKA1中用Tls实现,其中有些技巧。
在Symbian开发者网站的这个新栏目成立之初, Jo Stichbury开放诊所并提供与Symbian C++相关的建议。
在本月的诊所文章里,她研究了在Symbian OS DLL中使用可修改全局数据(也称为可写静态数据)的局限性。
新的代码诊所文章将于每月的第一个星期五发表。你有Symbian C++方面的问题?请将它发送至位于sdn@symbian.com的代码医生。
如果问题被接受,你会获得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 的成员用来向列
表中增加静态实例,但是没找到从列表中删除实例的成员函数。够用了,呵呵