Symbian中singleton的实现(多线程)


我正在将我的代码移植到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]讨论的一样。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值