cexer

cexer
posts - 12, comments - 334, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

转帖请注明出处 http://www.cppblog.com/cexer/archive/2008/07/08/55670.html

  单件(Singelton)模式可以说是众多设计模式当中,理解起来最容易,概念最为简单的一个。并且在实际的设计当中也是使用得又最为频繁的,甚至有很多其它的模式都要借助单件才能更好地实现。然而就是这样被强烈需求的“一句话模式”(一句话就能阐述明白),虽然有无数的牛人浸淫其中,至今也没有谁鼓捣出一个完美的实现。我小菜鸟一只自然更不敢逢人便谈单件。不过这个贴的主题是跟单件模式是密不可分的。

  什么又叫做“线程相关的单件模式”呢?也许你已经顾名思义猜出了八九分。不过还是允许我简单地用实例说明一下。

  假设你要设计了一个简单的 GUI 框架,这个框架当中需要这样一个全局变量(单件模式),它保存了所有窗口句柄与窗口指针的映射(我见过的数个的开源 GUI 框架都有类似的东西。)。在 WIN32 平台上就是这样一个简单的东西:

    //窗口的包装类
class Window
{
HWND m_hwnd;
public:
bool create();
bool destroy();

//其它细节
};

//窗口句柄与其对象指针的映射
typedef map<HWND,Window*> WindowMap;
typedef WindowMap::iterator WindowIter;
WindowMap theWindowMap;




  每创建一个窗口,就需要往这个 theWindowMap 当中添加映射。每销毁一个窗口,则需要从其中删除掉相关映射。实现代码类似:

    //创建窗口
bool Window::create()
{
m_hwnd=::CreateWindow(/*参数略*/);
if(!::IsWindow(m_hwnd))
return false;

theWindowMap[m_hwnd]=this; //添加映射
return true;
}

//销毁窗口
bool Window::destroy()
{
::DestroyWindow(m_hwnd);

theWindowMap.erase(m_hwnd); //删除映射
return true;
}


  你可以用任何可能的单件模式来实现这样一个全局变量 theWindowMap,它会 工作得很好。但是当如果考虑要给程序添加多线程支持(“多线程”是如此麻烦,它总爱和“但是”一起出现,给本来进行得很顺利的事情引起波折。),就会发现此时也许纯粹的单件模式并不是最好的选择。例如一个线程同时创建窗口,那么两个线程同时调用:

    theWindowMap[m_hwnd]=this;


  这显然不是一个原子操作,可以肯定如果你坚持这样干你的程序会慢慢走向崩溃,幸运一点只是程序运行结果错误,如果你恰好那几天印堂发暗面色发灰,说不定就因为这小小的错误,被无良的BOSS作为借口开除掉了,那可是个悲惨的结局。

  当然大多数的单件模式已经考虑到了多线程的问题。它们的解决方案就是给加上线程锁 ,我在数个开源的 GUI 框架看到他们都采用了这种解决方案。不过这样做,在线程同步过程当中,会产生与 GUI 框架逻辑不相关的同步消耗,虽然不是什么大不了的消耗,但是客户可能因此就选择了你的竟争对手,如果线程竟争激烈,在强烈渴求资源的环境(如小型移动设置)当中,这种消耗更是不可忽视的。

  实际上在应用当中,极少有线程需要插入删除其它线程创建的窗口映射(如果确实有这种需要,那么可以肯定项目的设计上出了问题)。在这种情况下本线程创建窗口映射都将只是本线程存取,类似“Thread-Specific”的概念。也就是说,theWindowMap 当中其它线程创建的窗口的映射对于本线程来说都是不需关心的,我们却要为那部分不必要东西整天提心吊胆并付出运行时消耗的代价,这也有点像“穿着棉袄洗澡”。但是怎么样才能做到更轻松爽快些呢?

  就本例问题而言,我们需要这样一种变量来保存窗口映射,它针对每个线程有不同的值(Thread-Specific Data),这些值互不影响,并且所有线程对它的访问如同是在访问一个进程内的全局变量(Singelton)。

  如果你是熟悉多线程编程的人,那么“Thread-Specific ”一定让你想起了什么。是的,“Thread-Specific Storage ” (线程相关存存诸,简称 TSS ),正是我们需要的,这是大多数操作系统都提供了的一种线程公共资源安全机制,这种机制允许以一定方式创建一个变量,这个变量在所在进程当中的每个线程当中,可以拥有不同的值。在 WIN32 上,这个变量就称为“索引”,其相关的值则称为“槽”, “Thread-Local Storage”(线程局部存诸,简称 TLS )机制。它的提了供这样几个函数来定义,设置,读取线程相关数据(关于 TLS 的更多信息,可以查阅 MSDN ):

    //申请一个“槽”的索引。
DWORD TlsAlloc( void );

//获得调用线程当中指定“槽”的值。
VOID* TlsGetValue( DWORD dwTlsIndex );

//设置调用线程当中指定“槽”的值。
BOOL TlsSetValue( DWORD dwTlsIndex,VOID* lpTlsValue );

//释放掉申请的“槽”的索引
BOOL TlsFree( DWORD dwTlsIndex );

  具体使用流程方法:先调用 TlsAlloc 申请一个“索引”,然后线程在适当时机创建一个对象并调用 TlsSetValue 将“索引”对应的“槽”设置为该对象的指针,在此之后即可用 TlsGetValue 访问该“糟”。最后在不需要的时候调用 TlsFree ,如在本例当中,调用 TlsFree 的最佳时机是在进程结束时。

  先封装一下 TlsAlloc 和 TlsFree  以方便对 ”索引“的管理。

    class TlsIndex
{
public:
TlsIndex()
:m_index(::TlsAlloc())
{}

~TlsIndex()
{
::TlsFree(m_index);
}

public:
operator DWORD() const
{
return m_index;
}

private:
DWORD m_index;
};

  
  如你所见,类 TlsIndex 将在构造的时候申请一个“索引”,在析构的时候释放此“索引”。

  在本例当中 TlsIndex 的对象应该存在进程的生命周内,以保证在进程退出之前,这个“索引”都不会被释放,这样的 TlsIndex 对象听起来正像一个全局静态对象,不过 Meyers Singelton (用函数内的静态对象实现)在这里会更适合,因为我们不需要对这个对象的生命周末进行精确控制,只需要它在需要的时候创建,然后在进程结束前销毁即可。这种方式只需要很少的代码即可实现,比如:

    DWORD windowMapTlsIndex()
{
static TlsIndex s_ti;  //提供自动管理生命周期的“索引”
return s_ti;
}


  利用这个“索引”,我们就能实现上述“Thread-Specific”的功能:

    WindowMap* windowMap()
{
WindowMap* wp=reinterpret_cast<WindowMap*>(::TlsGetValue(windowMapTlsIndex()));
if(!wp)
{
wp=new WindowMap();
::TlsSetValue(windowMapTlsIndex(),wp);
}
return wp;
}

#define theWindowMap *(windowMap())

  
  注意各线程访问以上的代码不会存在竟争。这样就实现了一个线程安全且无线程同步消耗版本的“全局对象” theWindowMap 。我们甚至不用改变Window::create,Window::destory,queryWindow 的代码,

  这几个简单的函数看起来似乎不像一个“模式”,但是它确实是的。

  现在总结一下“线程相关的单件模式”的概念:保证一个类在一个线程当中只有一个实例,并提供一个访问它的线程内的访问点的模式。

  为了不重复地制造车轮,我将此类应用的模式封装了一下:

    template<typename TDerived>
class TlsSingelton
{
typedef TDerived _Derived;
typedef TlsSingelton<TDerived> _Base;

public:
static _Derived* tlsInstance()
{
return tlsCreate();
}

protected:
static _Derived* tlsCreate()
{
_Derived* derived=tlsGet();
if(derived)
return derived;

derived=new _Derived();
if(derived && TRUE==::TlsSetValue(tlsIndex(),derived))
return derived;

if(derived)
delete derived;

return NULL;
}

static bool tlsDestroy()
{
_Derived* derived=tlsGet();
if(!derived)
return false;

delete derived;
return true;
}

static DWORD tlsIndex()
{
static TlsIndex s_tlsIndex;
return s_tlsIndex;
}

private:
static _Derived* tlsGet()
{
return reinterpret_cast<_Derived*>(::TlsGetValue(tlsIndex()));
}

static bool tlsSet(_Derived* derived)
{
return TRUE==::TlsSetValue(tlsIndex(),derived);
}

//noncopyable
private:
TlsSingelton(const _Base&);
TlsSingelton& operator=(const _Base&);
};


  将 tlsCreate,tlsDestroy 两个函数设置为保护成员,是为了防止一些不三不四吊尔啷噹的程序随意地删除。

  示例:

    class WindowMapImpl:public TlsSingelton<WindowMap>
{
WindowMap m_map;
public:
WidnowMap& theWindowMapImpl()
{
return m_map;
}

public:
~WindowMapImpl();

protected:
WindowMapImpl(); //只能通过tlsCreate创建
friend class _Base;
};

#define theWindowMap (WindowMapImpl::tlsInstance()->theWindowMapImpl())



  仍不需要修改原有窗口代码。

Feedback

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-09 16:57 by www.helpsoff.com.cn
哈哈,有意思!看到一半的时候,心想有必要引入tls吗,只要设好编译选项,map应该是线程安全的呀;不过看到后面,把tls引入然后封装到原有实现上去,觉得很精彩,其实已经跳出了讨论所谓singleton和线程安全的范畴了,虽然目的是这个。

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-09 18:32 by cexer
呵呵谢谢耐心看完。map可以通过编译器设置线程安全?我还不知道呢。不过引入tls与“线程相关的单件”,是为了可以让更多更复杂的此类应用更容易实现。还有map的线程安全是由实现提供的(非标准),应该不是所有编译器都支持的吧?

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-10 01:44 by www.helpsoff.com.cn
我大致记得用cl编译链接生成binary的时候带"/MT"就是连接多线程库,不过印象不深了,博主有兴趣可以看看。

我同意博主的说法,引入tls确实是为实现类似应用做好了封装。

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-10 09:02 by cexer
@www.helpsoff.com.cn
这个"/MT"应该指的链接是微软运行时库,C运行时库。C++标准库对包括map在内的容器在多线程环境当中的情况没有采取任何的保护措施。

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-10 14:09 by www.helpsoff.com.cn
对头,/MT是指连接微软的runtime lib,相对于标准库来将,这个库的实现是支持多线程的。不过博主说的对,连接这个库并不能保证map是线程安全了。献丑了...

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-10 22:53 by 梦在天涯
TlsObject<***> 这个东东哪里来的那,TLS倒是蛮好用的哦!

http://www.cppblog.com/mzty/archive/2007/08/01/28892.html

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2008-07-10 23:22 by cexer
@梦在天涯
应该是TlsSingelton,我写错了哈,谢谢提醒。

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2009-08-31 22:42 by stidio
这样写,用处不大,其实线程局部化本身用处就不大
如果这样写,同一个线程中持有的是不同的映射实例,而很多情况(不仅仅是窗口映射),这样做的目的,是为了根据一个index获得一个结果,也就是查询;
如果这样最,对于跨线程查询,你必须破坏你的设计;

不知道是不是我理解的问题,单件模式的引出,是为了确定资源的唯一性;而你的这个恰恰不是;例如:
张三,李四的老板是王五,那王五对于张三,李四来说,是他的"单件"
张7,张8的爸爸是张9,那张9是单件

而你却构建了一个,张9和王五的集合,说这是另外4个人的单件,这并不符合唯一性条件;

关于单件模型的多线程问题,其实单件本身没多线程问题,多线程问题的引入是在对单件对象的使用上;如果说单件存在着多线程问题,那也仅仅需要在创建时锁定(比如说2个线程同时获得,都为空,创建2次;这样需要在创建时锁定,并做二次判断,如
if(xx == 0) {
lock();
if(xx == 0)
xx = new XX;
....
}
而其实大多数情况不需要这样来折腾;


超哥不错哈,出去后的确进步了很多;

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2009-09-01 12:15 by cexer
@stidio
这个设计是设计给不跨线程的应用的,主要考虑在这种应用下,如果线程太多,都去查询同一个全局的东西,大多数线程都一直处于等待资源的状态,比较浪费CPU时间。
这是在公司时写的哈,出来后倒没写了,感觉是人越来越懒了,写程序越来越没激情

# re: 线程相关的单件模式(Thread-Specific Singelton)  回复  更多评论   

2010-05-12 16:10 by ZeroQ
#define theWindowMap (WindowMapImpl::tlsInstance()->theWindowMapImpl())
上面一行中,WindowMapImpl::tlsInstance()返回的是WindowMap实例,而WindowMap并没有theWindowMapImpl()方法,不知道这样是如何实现的。请教喽。。。

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理