POCO SocketReactor框架

今天分析了下POCO
SocketReactor类,是一种Reactor反应器模式的使用。主要有两点十分值得了解:
1. Reactor模式的实现方法。
2. POCO ThreadImp类中对TLS (Thread Local storage)的使用。
class Foundation_API ThreadImpl
{
private:
class CurrentThreadHolder
{
public:
CurrentThreadHolder(): _slot(TlsAlloc())
{
if (_slot == TLS_OUT_OF_INDEXES)
throw SystemException("cannot allocate thread context
key");
}
~CurrentThreadHolder()
{
TlsFree(_slot);
}
ThreadImpl* get() const
{
return reinterpret_cast(TlsGetValue(_slot));
}
void set(ThreadImpl* pThread)
{
TlsSetValue(_slot, pThread);
}
private:
DWORD _slot;
};
static CurrentThreadHolder _currentThreadHolder;
};
可以看到ThreadImpl类中定义了一个CurrentThreadHolder 静态成员,用于存储实际线程的HANDLE
_thread,用可以通过静态成员函数ThreadImpl*
ThreadImpl::currentImpl()随时访问。
开始看到这里会非常疑惑,用类的静态成员变量存储线程的HANDLE
_thread,那正常一个类只会共享一个_currentThreadHolder实例,多个线程的HANDLE根本无法使用,会错误获取非本线程的HANDLE。
仔细看CurrentThreadHolder这一内部类将线程存在一个DWORD _slot变量中,但是非常奇怪的是用
TlsAlloc、TlsGetValue、TlsSetValue、TlsFree几个Windows函数在进行管理,其中必有玄机。
搜索后发现使用TLS机制可以让进程的各个线程拥有线程局部的静态变量,这样_slot相当于每个线程拥有自己的实例,不会彼此间错误修改。
以下是网上搜到的TLS介绍:
为什么要有TLS?原因在于,进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量。在一个线程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交换变得非常快捷。说它是缺点,一个线程死掉了,其它线程也性命不保;
多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相关的BUG。
如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为static memory local
to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS。
线程局部存储在不同的平台有不同的实现,可移植性不太好。幸好要实现线程局部存储并不难,最简单的办法就是建立一个全局表,通过当前线程ID去查询相应的数据,因为各个线程的ID不同,查到的数据自然也不同了。
大多数平台都提供了线程局部存储的方法,无需要我们自己去实现:
linux:
int pthread_key_create(pthread_key_t *key, void
(*destructor)(void*));
int pthread_key_delete(pthread_key_t key);
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void
*value);
Win32
方法一:每个线程创建时系统给它分配一个LPVOID指针的数组(叫做TLS数组),这个数组从C编程角度是隐藏着的不能直接访问,需要通过一些C
API函数调用访问。首先定义一些DWORD线程全局变量或函数静态变量,准备作为各个线程访问自己的TLS数组的索引变量。一个线程使用TLS时,第一步在线程内调用TlsAlloc()函数,为一个TLS数组索引变量与这个线程的TLS数组的某个槽(slot)关联起来,例如获得一个索引变量:
global_dwTLSindex=TLSAlloc();
注意,此步之后,当前线程实际上访问的是这个TLS数组索引变量的线程内的拷贝版本。也就说,不同线程虽然看起来用的是同名的TLS数组索引变量,但实际上各个线程得到的可能是不同DWORD值。其意义在于,每个使用TLS的线程获得了一个DWORD类型的线程局部静态变量作为TLS数组的索引变量。C/C++原本没有直接定义线程局部静态变量的机制,所以在如此大费周折。
第二步,为当前线程动态分配一块内存区域(使用LocalAlloc()函数调用),然后把指向这块内存区域的指针放入TLS数组相应的槽中(使用TlsValue()函数调用)。
第三步,在当前线程的任何函数内,都可以通过TLS数组的索引变量,使用TlsGetValue()函数得到上一步的那块内存区域的指针,然后就可以进行内存区域的读写操作了。这就实现了在一个线程内部这个范围处处可访问的变量。
最后,如果不再需要上述线程局部静态变量,要动态释放掉这块内存区域(使用LocalFree()函数),然后从TLS数组中放弃对应的槽(使用TlsFree()函数)。
TLS 是一个良好的Win32 特质,让多线程程序设计更容易一些。TLS
是一个机制,经由它,程序可以拥有全域变量,但处于「每一线程各不相同」的状态。也就是说,进程中的所有线程都可以拥有全域变量,但这些变量其实是特定对某个线程才有意义。例如,你可能有一个多线程程序,每一个线程都对不同的文件写文件(也因此它们使用不同的文件handle)。这种情况下,把每一个线程所使用的文件handle
储存在TLS 中,将会十分方便。当线程需要知道所使用的handle,它可以从TLS 获得。重点在于:线程用来取得文件handle
的那一段码在任何情况下都是相同的,而从TLS中取出的文件handle 却各不相同。非常灵巧,不是吗?有全域变数的便利,却又分属各线程。
下面的四个函数就是对TLS进行操作的:
(1)TlsAlloc
上面我们说过了KERNEL32 使用两个DWORDs(总共64 个位)来记录哪一个slot 是可用的、哪一个slot
已经被用。当你需要使用一个TLS slot 的时候,你就可以用这个函数将相应的TLS slot位置1。
(2)TlsSetValue
TlsSetValue 可以把数据放入先前配置到的TLS slot 中。两个参数分别是TLS slot
索引值以及欲写入的数据内容。TlsSetValue 就把你指定的数据放入64 DWORDs 所组成的数组(位于目前的thread
database)的适当位置中。
(3)TlsGetValue
这个函数几乎是TlsSetValue 的一面镜子,最大的差异是它取出数据而非设定数据。和TlsSetValue
一样,这个函数也是先检查TLS 索引值合法与否。如果是,TlsGetValue 就使用这个索引值找到64 DWORDs
数组(位于thread database 中)的对应数据项,并将其内容传回。
(4)TlsFree
这个函数将TlsAlloc 和TlsSetValue 的努力全部抹消掉。TlsFree
先检验你交给它的索引值是否的确被配置过。如果是,它将对应的64 位TLS slots
位关闭。然后,为了避免那个已经不再合法的内容被使用,TlsFree 巡访进程中的每一个线程,把0 放到刚刚被释放的那个TLS slot
上头。于是呢,如果有某个TLS 索引后来又被重新配置,所有用到该索引的线程就保证会取回一个0
值,除非它们再调用TlsSetValue。
前一篇:一个终生受益的测试题