3.3设计自己的线程局部存储

TLS

在实际的应用过程中,往往使用 TLS(Thread Local Storage,线程局部存储)保存与各线程相关联的指针,指针指向的一组数据是在进程的堆中申请的。这样就可以保证,每个线程只访问与它相关联的指针指向的内存单元。为了简化这种使用 TLS 的过程,我们希望 TLS 具有以下两个的特性:

(1)自动管理它所保存的指针所指向的内存单元的分配和释放。这样做,一方面大大方便了用户使用,另一方面,在一个线程不使用线程局部变量的情况下,管理系统可以决定不为这个线程分配内存,从而节省内存空间。
(2)允许用户申请使用任意多个 TLS 索引。Microsoft 确保每个进程的位数组中至少有TLS_MINIMUM_AVAILABLE 个位标志是可用的。在 WinNT.h 文件中这个值被定义为 64,Windows 2000 又做了扩展,使至少 1000 个标志可用。

显然,为了实现这些新的特性,必须开发一个新的“TLS”。本节就讲述整个体系的设计过程。

总体来看,新的“TLS”主要由 4 个类组成,其中 CSimpleList 类负责实现简单的链表功能,把各线程私有数据连在一起,以便能够释放它们占用的内存;CNoTrackObject 类重载了new 和 delete 操作符,负责为线程私有数据分配内存空间;CThreadSlotData 类是整个系统的核心,它负责分配索引和存取线程私有数据;CThreadLocal 是最终提供给用户使用的类模板,它负责为用户提供友好的接口函数。

CSimpleList 类
从使用的角度看,通过一个全局索引,Windows 的 TLS 只允许用户保存一个 32 位的指针,而改进的系统允许用户保存任意类型的数据(包含整个类)。这个任意大小的数据所占的内存是在进程的堆中分配的,所以当用户释放全局索引时,系统必须将每个线程内此数据占用的内存释放掉,这就要求系统把为各线程分配的内存都记录下来。

较好的方法是将各个私有数据的首地址用一个链表连在一起,释放全局索引时只要遍历此链表,就可以逐个释放线程私有数据占用的空间了。例如,有下面一个存放线程私有数据的数据结构。

struct CThreadData 
{ 
     CThreadData* pNext; // 指向下一个线程的CThreadData 结构的指针
     LPVOID pData; // 指向真正的线程私有数据的指针
};

指针 pData 指向为线程分配的内存的首地址,指针 pNext 将各线程的数据连在了一起,如图 3.9 所示。只要通过第一个 CThreadData 结构的首地址 pHead 就可以管理整个表了。
3.3设计自己的线程局部存储
将各个 CThreadData 结构的数据连成一个表,移除、添加或获取表中的节都只与 pNext成员有关。换句话说,所有对这个表的操作都是依靠存取 pNext 成员的值来实现的。只要指定pNext 成员在 CThreadData 结构中的偏移量,就可以操作整个链表。更通用一点,给定一个数据结构,只要知道数据结构中 pNext 成员的偏移量(此结构必须包含一个 pNext 成员),就可以将符合此结构类型的数据连成一个表。这项功能有着很强的独立性,所以要写一个类来专门管理数据结构中的 pNext 成员,进而管理整个链表。

这个类需要知道的惟一信息就是CThreadData 结构中pNext 成员的偏移量。指定偏移量后,它就可以得到每个数据结构中 pNext 变量的地址,也就可以存取它的值了。如果再记录下链表中第一个数据的地址,整个类基本就可以实现。所以,用以实现类的成员被设计为以下 3 个。

void* m_pHead; // 链表中第一个元素的地址
size_t m_nNextOffset; // 数据结构中pNext 成员的偏移量
void** GetNextPtr(void* p) const 
{ 
    return (void**)((BYTE*)p + m_nNextOffset); 
}

GetNextPtr 函数通过偏移量 m_nNextOffset 取得 pNext 指针的地址,所以它的返回值是指向指针的指针。比如,传递了一个指向 CThreadData 类型数据的指针,GetNextPtr 函数将这个地址加上偏移量就得到了成员 pNext 指针的地址。

由于这个类仅实现了简单的链表的功能,所以为它命名为 CSimpleList。以字母“C”开头是本书给类命名的一个规则,比如后面还有 CWinThread、CWnd 类等。现在我们关心的问题是 CSimpleList 类应向用户提供什么样的接口函数。

一般对链表的操作有添加、删除和遍历表中的元素等。虽然只是对表进行简单的管理,CSimpleList 类也应该实现这些功能。完成这些功能的成员函数就是给用户提供的接口。

给定义类和实现类的文件命名也是一件比较重要的事情,同样,我们应该遵从一些规则才能使许多文件放在一起不至于发生混乱。封装这些类都是为了实现最后的 TLS 系统,所以我们将本节所设计的类的定义文件命名为_AFXTLS_.H。“AFX”对应的英文单词为 Application Framework,剩下的“X”是充数用的(三个字母组成一组比两个字母要好看一点)。今后我们类库中的头文件的文件名都以“AFX”开头。别把最前面那个的“”符号去掉,否则会和MFC(Microsoft Foundation Classes 的缩写)中的文件名发生冲突。MFC 是 VC 自带的一个类库,现在还不是介绍它的时候。下面是_AFXTLS_.H 头文件中的全部内容。

#ifndef __AFXTLS_H__  // _AFXTLS_.H 文件
#define __AFXTLS_H__

#include <windows.h>
#include <stddef.h>

class CSimpleList
{
public:
	CSimpleList(int nNextOffset = 0);
	void Construct(int nNextOffset);

//提供给用户的接口函数(Operations),用于添加、删除和遍历节点
	BOOL IsEmpty() const;
	void AddHead(void* p);
	void RemoveAll();
	void* GetHead() const;
	void* GetNext(void* p) const;
	BOOL Remove(void* p);

//为实现接口函数所需的成员(Implementation)
	void* m_pHead;		//链表中第一个元素的地址
	size_t m_nNextOffset;	//数据结构中pNext成员的偏移量
	void** GetNextPtr(void* p) const;
};

// 类的内联函数
inline CSimpleList::CSimpleList(int nNextOffset)
{ m_pHead = NULL; m_nNextOffset = nNextOffset; }

inline void CSimpleList::Construct(int nNextOffset)
{ m_nNextOffset = nNextOffset; }

inline BOOL CSimpleList::IsEmpty() const
{ return m_pHead == NULL; }

inline void CSimpleList::RemoveAll()
{ m_pHead = NULL; }

inline void* CSimpleList::GetHead() const
{ return m_pHead; }

inline void* CSimpleList::GetNext(void* preElement) const
{ return *GetNextPtr(preElement); }

inline void** CSimpleList::GetNextPtr(void* p) const
{ return (void**)((BYTE*)p + m_nNextOffset); }

#endif // __AFXTLS_H__

为了避免重复包含,AFXTLS.H 头文件使用了下面一组预编译指令。

#ifndef __AFXTLS_H__ // _AFXTLS_.H 文件
#define __AFXTLS_H__ 
………… // 具体申明
#endif // __AFXTLS_H__

编译预处理是模块化程序设计的一个重要工具。上面的预编译指令的意思是:如果没有定义宏名 __AFXTLS_H__, 就定义此宏名并编译 #endif 之前的代码; 如果定义了宏名__AFXTLS_H__的话,编译器就对#endif 之前的代码什么也不做。这就可以防止头文件_AFXTLS_.H 中的代码被重复包含(这样会引起编译错误)。标识每个头文件是否被包含的宏名应该遵从一定的规律,采用有意义的字符串,以提高程序的可读性和避免宏名的重复使用。如果没有什么特殊情况,最好采用本书所使用的命名规则。

实现 CSimpleList 类最关键的成员 m_nNextOffset 在类的构造函数里要被初始化为用户指定的值。成员变量 m_pHead 要被初始化为 NULL,表示整个链表为空。

AddHead 函数用于向链表中添加一个元素,并把新添加的元素放在表头。它的实现代码在 AFXTLS.CPP 文件中。

void CSimpleList::AddHead(void* p)
{
	*GetNextPtr(p) = m_pHead;
	m_pHead = p;
}

比如用户为这个函数传递了一个 CThreadData 类型的指针 p,AddHead 函数在将 p 设为新的表头前,必须使 p 的 pNext 成员指向原来的表头。这里,GetNextPtr 函数取得了 p 所指向的CThreadData 结构中 pNext 成员的地址。通过不断对 AddHead 函数的调用就可以将所有CThreadData 类型的数据连成一个链表了。

RemoveAll 函数最简单了,它仅把成员 m_pHead 的值设为 NULL,表示整个链表为空。GetHead 函数用于取得表中头元素的指针,和 GetNext 函数在一块使用可以遍历整个链表。

最后一个是 Remove 函数,它用来从表中删除一个指定的元素,实现代码在 AFXTLS.CPP文件中。

BOOL CSimpleList::Remove(void* p)
{
	if(p == NULL)	//检查参数
		return FALSE;
	
	BOOL bResult = FALSE; //假设移除失败
	if(p == m_pHead)
	{
	//要移除头元素
		m_pHead = *GetNextPtr(p);
		bResult = TRUE;
	}
	else
	{
		//试图在表中查找要移除的元素
		void* pTest = m_pHead;
		while(pTest != NULL && *GetNextPtr(pTest) != p)
			pTest = *GetNextPtr(pTest);

		//如果找到,就将元素移除
		if(pTest != NULL)
		{
			*GetNextPtr(pTest) = *GetNextPtr(p);
			bResult = TRUE;
		}
	}
	return bResult;
}

还假设参数 p 是一个 CThreadData 类型的指针。根据要移除的元素在表中位置,应该分 3种情况处理:
(1)此元素就是表头元素,这时只将下一个元素作为表头元素就可以了。
(2)此元素在链表中但不是表头元素,这种情况下,查找结束后 pTest 指针所指的元素就是要删除元素的前一个元素,所以只要让 pTest 所指元素的 pNext 成员指向要删除元素的下一个元素就可以了。
(3)表中根本不存在要删除的元素。

下面的小例子说明了使用 CSimpleList 类构建链表的方法。它通过 CSimpleList 类将一组自定义类型的数据连成链表,接着又遍历这个链表,打印出链表项的值。源程序代码如下。

template<class TYPE>
class CTypedSimpleList : public CSimpleList
{
public:
	CTypedSimpleList(int nNextOffset = 0) 
		: CSimpleList(nNextOffset) { }
	void AddHead(TYPE p) 
		{ CSimpleList::AddHead((void*)p); }
	TYPE GetHead()
		{ return (TYPE)CSimpleList::GetHead(); }
	TYPE GetNext(TYPE p)
		{ return (TYPE)CSimpleList::GetNext(p); }
	BOOL Remove(TYPE p)
		{ return CSimpleList::Remove(p); }
	operator TYPE()
		{ return (TYPE)CSimpleList::GetHead(); }
};

CTypedSimpleList 类将 CSimpleList 类中存在数据类型接口的函数都重载了,并在内部进行了类型的转换工作。上面的例子如果用 CTypedSimpleList 类实现会更简单,以下是改写后的部分代码段。

MyThreadData* pData; 
CTypedSimpleList<MyThreadData*> list; // 注意定义类模板对象的格式
list.Construct(offsetof(MyThreadData, pNext)); 
// 向链表中添加成员
for(int i=0; i<10; i++) 
{ //...... 同上 } 
 // ………… // 使用链表中的数据
 // 遍历整个链表,释放MyThreadData 对象占用的空间
pData = list; // 调用了成员函数 operator TYPE(),相当于“pData = list.GetHead();”语句
while(pData != NULL) 
{ //...... 同上 }

这段代码和上面的代码的执行结果是相同的。在语句“pData = list”中,程序直接引用了CTypedSimpleList 类的对象,这会导致其成员函数 operator TYPE()被调用,返回链表中第一个元素的指针(表头指针)。

CNoTrackObject 类
上一小节解决了将多个线程私有数据占用的内存连在一起的问题,这一小节来研究如何为线程私有数据分配内存。

C++语言中缺省版本的 operator new 是一种通用类型的内存分配器,它必须能够分配任意大小的内存块。同样 operator delete 也要可以释放任意大小的内存块。operator delete 想弄清它要释放的内存有多大,就必须知道当初 operator new 分配的内存有多大。有一种常用的方法可以让 operator new 来告诉 operator delete 当初分配内存的大小,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。也就是说,当写了下面的语句:

CThreadData* pData = new CThreadData;

得到的是像这样的内存块:
3.3设计自己的线程局部存储
运行在调试环境时,operator new 增加的额外内存就更多了,它需记录使用 new 申请内存的代码所在的文件及行号等,以便跟踪内存泄漏的情况。

线程私有数据使用的内存是由我们的系统在内部自动为用户分配的。当使用私有数据的线
程结束时这个系统也会为用户自动释放掉这块内存。用户不必关心这一过程,他们只要在愿意时使用线程私有数据就行了。确保用户的操作不会发生内存泄漏是我们 TLS 系统的责任。既然我们可以确保不会发生内存泄漏,也就没有必要跟踪内存的使用了。为了节省内存空间,也不需要记录内存的大小。所有这一切说明应该自己写 operator new 和 operator delete 的实现代码,直接使用 API 函数提供一个低层内存分配器(Low_level alloctor)。

要保证所有的线程私有数据内存的分配和释放都由重写的 new 和 delete 操作符来完成,
这很简单,编写一个重载 new 和 delete 操作符的基类,让所有线程私有数据使用的结构都从此类继承即可。此类的类名应该能够反映出自己使用的内存没有被跟踪的特点,所以为它命名为 CNoTrackObject。下面是 CNoTrackObject 类的定义和实现代码,还是在_AFXTLS.H 和AFXTLS.CPP 文件中。

//_AFXTLS.H 文件
class CNoTrackObject
{
public:
	void* operator new(size_t nSize);
	void operator delete(void*);
	virtual ~CNoTrackObject() { }
};

//AFXTLS.CPP 文件
void* CNoTrackObject::operator new(size_t nSize)
{
	// 申请一块带有GMEM_FIXED和GMEM_ZEROINIT标志的内存
	void* p = ::GlobalAlloc(GPTR, nSize);
	return p;
}

void CNoTrackObject::operator delete(void* p)
{
	if(p != NULL)
		::GlobalFree(p);
}

GlobalAlloc 函数用于在进程的默认堆中分配指定大小的内存空间,原型如下。

HGLOBAL GlobalAlloc( 
  UINT uFlags, //指定内存属性
  SIZE_T dwBytes //执行内存大小
);

uFlags 参数可以是下列值的组合:
● GHND GMEM_MOVEABLE 和 GMEM_ZEROINIT 的组合
● GMEM_FIXED 申请固定内存,函数返回值是内存指针
● GMEM_MOVEABLE 申请可移动内存,函数返回值是到内存对象的句柄,为了将句柄转化成指针,使用 GlobalLock 函数即可
● GMEM_ZEROINIT 初始化内存内容为 0
● GPTR GMEM_FIXED 和 GMEM_ZEROINIT 的组合
可移动内存块永远不会在物理内存中,在使用之前,必须调用 GlobalLock 函数把它锁定到物理内存。不再使用时应调用 GlobalUnlock 函数解锁。

GlobalFree 函数用于释放 GlobalAlloc 函数分配的内存空间。

再以 CThreadData 结构为例,为了让它的对象占用的内存使用 CNoTrackObject 类提供的内存分配器分配,需要将它定义为 CNoTrackObject 类的派生类。

在这里插入代码片

有了以上的定义,程序中再出现下面的代码的时候 Windows 会调用 CNoTrackObject 类中的函数去管理内存。

CThreadData* pData = new CThreadData; 
// ... // 使用CThreadData 对象
delete pData;

当 C++编译器看到第一行时,它查看 CThreadData 类是否包含或者继承了重载 new 操作符的成员函数。如果包含,那么编译器就生产调用该函数的代码,如果编译器不能找到重载new 操作符的函数,就会产生调用标准 new 操作符函数的代码。编译器也是同样看待 delete操作符的。上面的代码当然会调用 CNoTrackObject 类中的 operator new 函数去为 CThreadData对象申请内存空间。

这种做法很完美,只要是从 CNoTrackObject 派生的类都可以作为线程私有数据的数据类型。我们的系统也是这么要求用户定义线程私有数据的类型的。

CThreadSlotData 类
上面已经谈到,线程局部存储所使用的内存都以一个 CThreadData 结构开头,其成员指针pData 指向真正的线程私有数据。如果再把 pData 指向的空间分成多个槽(slot),每个槽放一个线程私有数据指针,就可以允许每个线程存放任意个线程私有指针了。

把 pData 指向的空间分成多个槽很简单,只要把这个空间看成是 PVOID 类型的数组就行了。数组中的每一个元素保存一个指针,即线程私有数据指针,该指针指向在堆中分配的真正存储线程私有数据的内存块。在 CThreadData 结构中要保存的信息就是指针数组的首地址和数组的个数,所以将 CThreadData 结构改写如下。

struct CThreadData : public CNoTrackObject
{
	CThreadData* pNext; //CSimpleList类要使用此成员
	int nCount;	    //数组元素的个数
	LPVOID* pData;      //数组的首地址
};

现在,各个线程的线程私有空间都要以这个新的 CThreadData 结构开头了。pData 是一个PVOID 类型数组的首地址,数组的元素保存线程私有数据的指针。nCount 成员记录了此数组的个数,也就是槽的个数,它们的关系如图 3.11 所示。
3.3设计自己的线程局部存储
上图说明了我们所设计的 TLS 系统保存线程私有数据的最终形式。当用户请求访问本线程的私有数据时,应该说明要访问哪一个槽号对应的线程私有数据。我们的系统在内部首先得到该线程中 CThreadData 结构的首地址,再以用户提供的槽号作为 pData 的下标取得该槽号对应的线程私有数据的地址。此时有两个问题要解决:
(1)如何为用户分配槽号。
(2)如何保存各线程中 CThreadData 结构的首地址。

第 2 个问题很容易解决,直接利用 Windows 提供的 TLS 机制申请一个用于为各个线程保存 CThreadData 结构首地址的全局索引就行了。

要解决第 1 个问题,可以仿照 Windows 实现 TLS 时的做法,在进程内申请一个位数组,数组下标表示槽号,其成员的值来指示该槽号是否被分配。每个进程只有一份的这个数组当然不仅仅用来表示槽是否被占用,还可以用来表示其他信息,例如占用该槽的模块等,这只要改变数组的数据类型就可以了。我们的系统使用全局数组表示分配了哪一个槽,以及是为哪个模块分配的,所以数组的数据类型应该是这样的。

struct CSlotData 
{ 
    DWORD dwFlags; // 槽的使用标志(被分配/未被分配)
    HINSTANCE hInst; // 占用此槽的模块句柄
};

在进程内申请一个 CSlotData 类型的数组就可以管理各个线程使用的槽号了。先不要管hInst 成员的值(现在总把它设为 NULL)。我们规定,dwFlags 的值为 0x01 时表示该槽被分配,为 0 时表示该槽未被分配。这样一来,为用户分配槽号的问题也得到了解决。

到此为止,我们的 TLS 系统的功能基本都可以实现了。按照前面的设计要写一个类来管理各槽对应的指向线程私有数据的指针,具体来说,这个类要负责全局槽号的分配和各线程中槽里数据的存取,所以为它命名为 CThreadSlotData。

CThreadSlotData 类要以 Win32 的 TLS 为基础为每各个线程保存其线程私有数据的指针,所以类中应该有一个用做 TLS 索引的成员变量。

DWORD m_tlsIndex;

在类的构造函数中,我们要调用 TlsAlloc 函数分配索引,而在析构函数中又要调用 TlsFree函数释放此索引。当用户在线程中第一次访问线程私有变量的时候,我们的系统要为该线程的私有变量申请内存空间,并以 m_tlsIndex 为参数调用 TlsSetValue 函数保存此内存空间的地址。

为了将各个线程的私有数据串连起来,还要使用 CTypedSimpleList 类。

CTypedSimpleList<CThreadData*> m_list;

m_list 成员变量用来设置各个线程私有数据头部 CThreadData 结构中 pNext 成员的值。为一个线程的私有数据申请内存空间后,应立刻调用 m_list.AddHead 函数将该线程私有空间的首地址添加到 m_list 成员维护的链表中。

负责管理全局标志数组的成员有以下几个。

int m_nAlloc; //m_pSlotData 所指向数组的大小
int m_nMax; //占用的槽的最大数目
CSlotData* m_pSlotData; //全局数组的首地址

m_pSlotData 是数组的首地址。因为数组是动态分配的,所以还要记录下它的大小。成员m_nAlloc 表示数组成员的个数,m_nMax 表示迄今为止占用的槽的最大数目。例如,在一个时间里,CSlotData 数组的大小为 6,其槽的使用情况如下:
3.3设计自己的线程局部存储
这个时候,m_nAlloc 的值是 6,而 m_nMax 的值是 4。因为 m_nMax 的值是从 Slot0 算起到最后一个被使用过的槽(假定 Slot4 未被使用过)的数目。每个使用线程私有数据的线程都有一个 CThreadData 结构,结构中成员 pData 指向的空间被分为若干个槽。到底被分为多少个槽是由全局负责分配槽的数组的状态决定的。为了节省内存空间,我们规定,m_nMax 的值就是各个线程为其私有数据分配的槽的数量。

CThreadSlotData 类向用户提供的接口函数也应该和 Windows 的 TLS 提供的函数对应起来,提供分配、释放槽号及通过槽号访问线程私有数据的功能。设计完 CThreadSlotData 类的实现过程和接口函数,再真正写代码完成整个类就很容易了。下面是定义类的代码,同样应保存在_AFXTLS.H 文件中。

//CThreadSlotData - 管理我们自己的线程局部存储 // _AFXTLS.H 文件中
struct CSlotData;
struct CThreadData;

class CThreadSlotData
{
public:
	CThreadSlotData();

// 提供给用户的接口函数(Operations)
	int AllocSlot();	
	void FreeSlot(int nSlot); 
	void* GetThreadValue(int nSlot); 
	void SetValue(int nSlot, void* pValue);
	void DeleteValues(HINSTANCE hInst, BOOL bAll = FALSE);

// 类的实现代码(Implementations)
	DWORD m_tlsIndex;	// 用来访问系统提供的线程局部存储

	int m_nAlloc;		//  m_pSlotData所指向数组的大小
	int m_nRover;		// 为了快速找到一个空闲的槽而设定的值
	int m_nMax;		// CThreadData结构中pData指向的数组的大小
	CSlotData* m_pSlotData;	// 标识每个槽状态的全局数组的首地址
	CTypedSimpleList<CThreadData*> m_list;	// CThreadData结构的列表
	CRITICAL_SECTION m_cs;

	void* operator new(size_t, void* p)
			{ return p; }
	void DeleteValues(CThreadData* pData, HINSTANCE hInst);
	~CThreadSlotData();
};

m_cs 是一个关键段变量,在类的构造函数中被初始化。因为 CThreadSlotData 类定义的对象是全局变量,所以必须通过 m_cs 来同步多个线程对该变量的并发访问。

类中函数的实现代码在 AFXTLS.CPP 文件中,下面逐个来讨论。

//-------------------CThreadSlotData类----------------------//
BYTE __afxThreadData[sizeof(CThreadSlotData)];	// 为下面的_afxThreadData变量提供内存
CThreadSlotData* _afxThreadData; // 定义全局变量_afxThreadData来为全局变量分配空间

struct CSlotData
{
	DWORD dwFlags;	// 槽的使用标志(被分配/未被分配)
	HINSTANCE hInst;// 占用此槽的模块句柄
};

struct CThreadData : public CNoTrackObject
{
	CThreadData* pNext; // CSimpleList类要使用此成员
	int nCount;	    // 数组元素的个数
	LPVOID* pData;      // 数组的首地址
};

#define SLOT_USED 0x01		// CSlotData结构中dwFlags成员的值为0x01时表示该槽已被使用

CThreadSlotData::CThreadSlotData()
{
	m_list.Construct(offsetof(CThreadData, pNext)); // 初始化CTypedSimpleList对象

	m_nMax = 0;
	m_nAlloc = 0;
	m_nRover = 1;	// 我们假定Slot1还未被分配(第一个槽(Slot0)总是保留下来不被使用)
	m_pSlotData = NULL;

	m_tlsIndex = ::TlsAlloc();	// 使用系统的TLS申请一个索引
	::InitializeCriticalSection(&m_cs);	// 初始化关键段变量
}

首先定义 CThreadSlotData 类型的全局变量_afxThreadData 来为进程的线程分配线程局部存储空间。CThreadSlotData 类也重载了 new 运算符,但 operator new 函数并不真正为CThreadSlotData 对象分配空间,仅仅返回参数中的指针做为对象的首地址。例如,初始化_afxThreadData 指针所用的代码如下。

_afxThreadData = new(__afxThreadData) CThreadSlotData;

C++语言标准规定的 new 表达式的语法是:

[::] new [placement] new-type-name [new-initializer]

如果重载的 new 函数有除 size_t 以外的参数的话,要把它们写在 placement 域。type-name域指定了要被初始化的对象的类型。编译器在调用完 operator new 以后,还要调用此类型中的构造函数去初始化对象。如果类的构造函数需要传递参数的话,应该在 initializer 域指定。

用户在使用我们的线程局部存储系统时,必须首先调用 AllocSlot 申请一个槽号,下面是实现 AllocSlot 函数的代码。

int CThreadSlotData::AllocSlot()
{
	::EnterCriticalSection(&m_cs);	// 进入临界区(也叫关键段)
	int nAlloc = m_nAlloc;
	int nSlot = m_nRover;

	if(nSlot >= nAlloc || m_pSlotData[nSlot].dwFlags & SLOT_USED)
	{
		// 搜索m_pSlotData,查找空槽(SLOT)
		for(nSlot = 1; nSlot < nAlloc && m_pSlotData[nSlot].dwFlags & SLOT_USED; nSlot ++) ;

		// 如果不存在空槽,申请更多的空间
		if(nSlot >= nAlloc)
		{
			// 增加全局数组的大小,分配或再分配内存以创建新槽
			int nNewAlloc = nAlloc + 32;

			HGLOBAL hSlotData;
			if(m_pSlotData == NULL)	// 第一次使用
			{
				hSlotData = ::GlobalAlloc(GMEM_MOVEABLE, nNewAlloc*sizeof(CSlotData));
			}
			else
			{
				hSlotData = ::GlobalHandle(m_pSlotData);
				::GlobalUnlock(hSlotData);
				hSlotData = ::GlobalReAlloc(hSlotData, 
					nNewAlloc*sizeof(CSlotData), GMEM_MOVEABLE);
			}
			CSlotData* pSlotData = (CSlotData*)::GlobalLock(hSlotData);
	
			// 将新申请的空间初始化为0
			memset(pSlotData + m_nAlloc, 0, (nNewAlloc - nAlloc)*sizeof(CSlotData));
			m_nAlloc = nNewAlloc;
			m_pSlotData = pSlotData;
		}
	}

	// 调整m_nMax的值,以便为各线程的私有数据分配内存
	if(nSlot >= m_nMax)
		m_nMax = nSlot + 1;

	m_pSlotData[nSlot].dwFlags |= SLOT_USED;
	// 更新m_nRover的值(我们假设下一个槽未被使用)
	m_nRover = nSlot + 1;

	::LeaveCriticalSection(&m_cs);
	return nSlot; // 返回的槽号可以被FreeSlot, GetThreadValue, SetValue函数使用了
}

成员变量 m_nRover 是为了快速找到一个没有被使用的槽而设定的。我们总是假设当前所分配槽的下一个槽未被使用(绝大多数是这种情况)。

第一次进入时,我们申请 sizeof(CSlotData)*32 大小的空间用于表示各槽的状态。这块空间一共可以为用户分配 31 个槽号(Slot0 被保留)。用户使用完这 31 个槽还继续要求分配槽号的话,我们再重新申请内存空间以满足用户的要求。这种动态申请内存的方法就可以允许用户使用任意多个槽号了。

用户得到槽号后就可以访问该槽号对应的各线程的私有数据了,这个功能由 SetValue 函数来完成。同样,在用户第一次设置线程私有数据的值时,我们为该线程私有数据申请内存空间。下面是具体实现代码。

void CThreadSlotData::SetValue(int nSlot, void* pValue)
{
	// 通过TLS索引得到我们为线程安排的私有存储空间
	CThreadData* pData = (CThreadData*)::TlsGetValue(m_tlsIndex);

	// 为线程私有数据申请内存空间
	if((pData == NULL || nSlot >= pData->nCount) && pValue != NULL)
	{
		// pData的值为空,表示该线程第一次访问线程私有数据
		if(pData == NULL)
		{
			pData = new CThreadData;
			pData->nCount = 0;
			pData->pData = NULL;

			// 将新申请的内存的地址添加到全局列表中
			::EnterCriticalSection(&m_cs);
			m_list.AddHead(pData);
			::LeaveCriticalSection(&m_cs);
		}

		// pData->pData指向真正的线程私有数据,下面的代码将私有数据占用的空间增长到m_nMax指定的大小
		if(pData->pData == NULL)
			pData->pData = (void**)::GlobalAlloc(LMEM_FIXED, m_nMax*sizeof(LPVOID));
		else
			pData->pData = (void**)::GlobalReAlloc(pData->pData, m_nMax*sizeof(LPVOID), LMEM_MOVEABLE);
		
		// 将新申请的内存初始话为0
		memset(pData->pData + pData->nCount, 0, 
			(m_nMax - pData->nCount) * sizeof(LPVOID));
		pData->nCount = m_nMax;
		::TlsSetValue(m_tlsIndex, pData);
	}

	// 设置线程私有数据的值
	pData->pData[nSlot] = pValue;
}

CThreadSlotData 并不是最终提供给用户使用的存储线程局部变量的类。真正用户使用的存储数据的空间是各线程中 pData->pData[nSlot]指针指的内存块(见图 3.11 所示),即GetThreadValue 函数返回的指针指向的内存。CThreadSlotData 并不负责创建这块空间,但它负责释放这块空间使用的内存。所以在释放一个索引的时候,我们还要释放真正的用户数据使用的空间。下面的 FreeSlot 函数说明了这一点。

void CThreadSlotData::FreeSlot(int nSlot)
{
	::EnterCriticalSection(&m_cs);	

	// 删除所有线程中的数据
	CThreadData* pData = m_list;
	while(pData != NULL)
	{
		if(nSlot < pData->nCount)
		{
			delete (CNoTrackObject*)pData->pData[nSlot];
			pData->pData[nSlot] = NULL;
		}
		pData = pData->pNext;
	}

	// 将此槽号标识为未被使用
	m_pSlotData[nSlot].dwFlags &= ~SLOT_USED;
	::LeaveCriticalSection(&m_cs);
}

释放一个槽,意味着删除所有线程中此槽对应的用户数据。通过遍历 m_list 管理的链表很容易得到各线程中 CThreadData 结构的首地址,进而释放指定槽中的数据所指向的内存。

当线程结束的时候,就要释放此线程局部变量占用的全部空间。这正是我们的 TLS 系统实现自动管理内存的关键所在。CThreadSlotData 类用 DeleteValues 函数释放一个或全部线程因使用局部存储而占用的内存,代码如下。

void CThreadSlotData::DeleteValues(HINSTANCE hInst, BOOL bAll)
{
	::EnterCriticalSection(&m_cs);
	if(!bAll)
	{
		// 仅仅删除当前线程的线程局部存储占用的空间
		CThreadData* pData = (CThreadData*)::TlsGetValue(m_tlsIndex);
		if(pData != NULL)
			DeleteValues(pData, hInst);
	}
	else
	{
		// 删除所有线程的线程局部存储占用的空间
		CThreadData* pData = m_list.GetHead();
		while(pData != NULL)
		{
			CThreadData* pNextData = pData->pNext;
			DeleteValues(pData, hInst);
			pData = pNextData;
		}
	}
	::LeaveCriticalSection(&m_cs);
}

void CThreadSlotData::DeleteValues(CThreadData* pData, HINSTANCE hInst)
{
	// 释放表中的每一个元素
	BOOL bDelete = TRUE;
	for(int i=1; i<pData->nCount; i++)
	{
		if(hInst == NULL || m_pSlotData[i].hInst == hInst)
		{
			// hInst匹配,删除数据
			delete (CNoTrackObject*)pData->pData[i];
			pData->pData[i] = NULL;
		}
		else
		{
			// 还有其它模块在使用,不要删除数据
			if(pData->pData[i] != NULL)
			bDelete = FALSE;
		}
	}

	if(bDelete)
	{
		// 从列表中移除
		::EnterCriticalSection(&m_cs);
		m_list.Remove(pData);
		::LeaveCriticalSection(&m_cs);
		::LocalFree(pData->pData);
		delete pData;

		// 清除TLS索引,防止重用
		::TlsSetValue(m_tlsIndex, NULL);
	}
}

代码的注释非常详细,就不再多说了。类的析够函数要释放掉所有使用的内存,并且释放TLS 索引 m_tlsIndex 和移除临界区对象 m_cs,具体代码如下。

CThreadSlotData::~CThreadSlotData()
{
	CThreadData *pData = m_list;
	while(pData != NULL)
	{
		CThreadData* pDataNext = pData->pNext;
		DeleteValues(pData, NULL);
		pData = pData->pNext;
	}

	if(m_tlsIndex != (DWORD)-1)
		::TlsFree(m_tlsIndex);

	if(m_pSlotData != NULL)
	{
		HGLOBAL hSlotData = ::GlobalHandle(m_pSlotData);
		::GlobalUnlock(hSlotData);
		::GlobalFree(m_pSlotData);
	}

	::DeleteCriticalSection(&m_cs);
}

到此,CThreadSlotData 类的封装工作就全部完成了。有了这个类的支持,我们的线程局部存储系统很快就可以实现了。

CThreadLocal 类模板
CThreadSlotData 类没有实现为用户使用的数据分配内存空间的功能,这就不能完成允许用户定义任意类型的数据做为线程局部存储变量的初衷。这一小节再封装一个名称为CThreadLocal 的类模板来结束整个系统的设计。

CThreadLocal 是最终提供给用户使用的类模板。类名的字面意思就是“线程局部存储”。用户通过 CThreadLocal 类管理的是线程内各槽中数据所指的真正的用户数据。

现在我们着意于 CThreadLocal 类模板的设计过程。允许用户定义任意类型的线程私有变量是此类模板要实现的功能,这包括两方面的内容:
(1)在进程堆中,为每个使用线程私有变量的线程申请内存空间。这很简单,只要使用
new 操作符就行。
(2)将上面申请的内存空间的首地址与各线程对象关联起来,也就是要实现一个线程局部变量,保存上面申请的内存的地址。显然,CThreadSlotData 类就是为完成此功能而设计的。

保存内存地址是一项独立的工作,最好另外封装一个类来完成。这个类的用途是帮助CThreadLocal 类实现一个线程私有变量,我们就将它命名为 CThreadLocalObject。这个类也定义在_AFXTLS_.H 文件中,代码如下。

class CThreadLocalObject
{
public:
// 属性成员(Attributes),用于取得保存在线程局部的变量中的指针
	CNoTrackObject* GetData(CNoTrackObject* (*pfnCreateObject)());
	CNoTrackObject* GetDataNA();

// 具体实现(Implementation)
	DWORD m_nSlot;
	~CThreadLocalObject();
};

设计 CThreadLocalObject 类的目的是要它提供一个线程局部的变量。两个接口函数中,GetDataNA 用来返回变量的值;GetData 也可以返回变量的值,但是如果发现还没有给该变量分配槽号(m_nSlot == 0),则给它分配槽号;如果槽 m_nSlot 中还没有数据(为空),则调用参数 pfnCreateObject 传递的函数创建一个数据项,并保存到槽 m_nSlot 中。具体实现代码在AFXTLS.CPP 文件中。

CNoTrackObject* CThreadLocalObject::GetData(CNoTrackObject* (*pfnCreateObject)())
{
	if(m_nSlot == 0)
	{
		if(_afxThreadData == NULL)
			_afxThreadData = new(__afxThreadData) CThreadSlotData;
		m_nSlot = _afxThreadData->AllocSlot();
	}
 
	CNoTrackObject* pValue = (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);
	if(pValue == NULL)
	{
		// 创建一个数据项
		pValue = (*pfnCreateObject)();

		// 使用线程私有数据保存新创建的对象
		_afxThreadData->SetValue(m_nSlot, pValue);	
	}
	
	return pValue;
}

CNoTrackObject* CThreadLocalObject::GetDataNA()
{
	if(m_nSlot == 0 || _afxThreadData == 0)
		return NULL;
	return (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);
}

CThreadLocalObject::~CThreadLocalObject()
{
	if(m_nSlot != 0 && _afxThreadData != NULL)
		_afxThreadData->FreeSlot(m_nSlot);
	m_nSlot = 0;
}

CThreadLocalObject 类没有显式的构造函数,谁来负责将 m_nSlot 的值初始化为 0 呢?其实这和此类的使用方法有关。既然多个线程使用同一个此类的对象,当然要求用户将CThreadLocalObject 类的对象定义为全局变量了。全局变量的所有成员都会被自动初始化为 0。

最后一个类——CThreadLocal,只要提供为线程私有变量申请内存空间的函数,能够进行类型转化即可。下面是 CThreadLocal 类的实现代码。

template<class TYPE> 
class CThreadLocal : public CThreadLocalObject 
{
//属性成员(Attributes)
public: 
      TYPE* GetData() 
      { 
           TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject); 
           return pData; 
      } 
      TYPE* GetDataNA() 
      { 
           TYPE* pData = (TYPE*)CThreadLocalObject::GetDataNA(); 
           return pData; 
      } 
      operator TYPE*() 
      { 
           return GetData();
      } 
      TYPE* operator->() 
      { 
           return GetData(); 
      } 
// 具体实现(Implementation)
public: 
      static LPVOID CreateObject() 
      { 
           return new TYPE; 
      } 
};

CThreadLocal 模板可以用来声明任意类型的线程私有的变量,因为通过模板可以自动正确地转化(cast)指针类型。成员函数 CreateObject 用来创建动态指定类型的对象。成员函数GetData 调用了基类 CThreadLocalObject 的同名函数,并且把 CreateObject 函数的地址作为参数传递给它。

另外,CThreadLocal 模板重载了操作符号“*”、“->”,这样编译器将自动地进行有关类型
转换。

使用 CThreadLocal 类模板的时候,要首先从 CNoTrackObject 类派生一个类(结构),然后以该类做为 CThreadLocal 类模板的参数定义线程局部变量。下面是一个具体的例子。这个例子创建了 10 个辅助线程,这些线程先设置线程私有数据的值,然后通过一个公用的自定义函数 ShowData 将前面设置的值打印出来。可以看到,线程私有数据在不同线程中的取值可以是不同的。程序代码如下。

// MyTls.cpp文件
#include "_afxtls_.h"
#include <process.h>

#include <iostream>
using namespace std;

struct CMyThreadData : public CNoTrackObject
{
	int nSomeData;
};

// 下面的代码展开后相当于“CThreadLocal<CMyThreadData> g_myThreadData;”
THREAD_LOCAL(CMyThreadData, g_myThreadData)

void ShowData();
UINT __stdcall ThreadFunc(LPVOID lpParam)
{
	g_myThreadData->nSomeData = (int)lpParam;
	ShowData();
	return 0;
}

void main()
{
	HANDLE h[10];
	UINT uID;

	// 启动十个线程,将i做为线程函数的参数传过去
	for(int i = 0; i < 10; i++)
		h[i] = (HANDLE) ::_beginthreadex(NULL, 0, ThreadFunc, (void*)i, 0, &uID);
	::WaitForMultipleObjects(10, h, TRUE, INFINITE);
	for(int i = 0; i < 10; i++)
		::CloseHandle(h[i]);
}

void ShowData()
{
	int nData = g_myThreadData->nSomeData;
	printf(" Thread ID: %-5d, nSomeData = %d \n", ::GetCurrentThreadId(), nData);
}

用 CThreadLocal 实现线程本地存储方便了许多。但是,我们自己设计的 TLS 系统在一个使用过线程私有数据的线程运行结束后并没有释放该线程的数据所占用的内存。这就造成了内存泄漏。
_AFXTLS_.H

// _AFXTLS_.H文件
#ifndef __AFXTLS_H__  // _AFXTLS_.H 文件
#define __AFXTLS_H__

#include <windows.h>
#include <stddef.h>


class CNoTrackObject;


// CSimpleList

class CSimpleList
{
public:
	CSimpleList(int nNextOffset = 0);
	void Construct(int nNextOffset);

// 提供给用户的接口函数(Operations),用于添加、删除和遍历节点
	BOOL IsEmpty() const;
	void AddHead(void* p);
	void RemoveAll();
	void* GetHead() const;
	void* GetNext(void* p) const;
	BOOL Remove(void* p);

// 为实现接口函数所需的成员(Implementation)
	void* m_pHead;		// 链表中第一个元素的地址
	size_t m_nNextOffset;	// 数据结构中pNext成员的偏移量
	void** GetNextPtr(void* p) const;
};

// 类的内联函数
inline CSimpleList::CSimpleList(int nNextOffset)
{ m_pHead = NULL; m_nNextOffset = nNextOffset; }

inline void CSimpleList::Construct(int nNextOffset)
{ m_nNextOffset = nNextOffset; }

inline BOOL CSimpleList::IsEmpty() const
{ return m_pHead == NULL; }

inline void CSimpleList::RemoveAll()
{ m_pHead = NULL; }

inline void* CSimpleList::GetHead() const
{ return m_pHead; }

inline void* CSimpleList::GetNext(void* preElement) const
{ return *GetNextPtr(preElement); }

inline void** CSimpleList::GetNextPtr(void* p) const
{ return (void**)((BYTE*)p + m_nNextOffset); }


template<class TYPE>
class CTypedSimpleList : public CSimpleList
{
public:
	CTypedSimpleList(int nNextOffset = 0) 
		: CSimpleList(nNextOffset) { }
	void AddHead(TYPE p) 
		{ CSimpleList::AddHead((void*)p); }
	TYPE GetHead()
		{ return (TYPE)CSimpleList::GetHead(); }
	TYPE GetNext(TYPE p)
		{ return (TYPE)CSimpleList::GetNext(p); }
	BOOL Remove(TYPE p)
		{ return CSimpleList::Remove(p); }
	operator TYPE()
		{ return (TYPE)CSimpleList::GetHead(); }
};



// CNoTrackObject
class CNoTrackObject
{
public:
	void* operator new(size_t nSize);
	void operator delete(void*);
	virtual ~CNoTrackObject() { }
};

/
// CThreadSlotData - 管理我们自己的线程局部存储

// warning C4291: no matching operator delete found
#pragma warning(disable : 4291) 

struct CSlotData;
struct CThreadData;

class CThreadSlotData
{
public:
	CThreadSlotData();

// 提供给用户的接口函数(Operations)
	int AllocSlot();	
	void FreeSlot(int nSlot); 
	void* GetThreadValue(int nSlot); 
	void SetValue(int nSlot, void* pValue);
	void DeleteValues(HINSTANCE hInst, BOOL bAll = FALSE);

// 类的实现代码(Implementations)
	DWORD m_tlsIndex;	// 用来访问系统提供的线程局部存储

	int m_nAlloc;		//  m_pSlotData所指向数组的大小
	int m_nRover;		// 为了快速找到一个空闲的槽而设定的值
	int m_nMax;		// CThreadData结构中pData指向的数组的大小
	CSlotData* m_pSlotData;	// 标识每个槽状态的全局数组的首地址
	CTypedSimpleList<CThreadData*> m_list;	// CThreadData结构的列表
	CRITICAL_SECTION m_cs;

	void* operator new(size_t, void* p)
			{ return p; }
	void DeleteValues(CThreadData* pData, HINSTANCE hInst);
	~CThreadSlotData();
};


///

class CThreadLocalObject
{
public:
// 属性成员(Attributes),用于取得保存在线程局部的变量中的指针
	CNoTrackObject* GetData(CNoTrackObject* (*pfnCreateObject)());
	CNoTrackObject* GetDataNA();

// 具体实现(Implementation)
	DWORD m_nSlot;
	~CThreadLocalObject();
};


template<class TYPE>
class CThreadLocal : public CThreadLocalObject
{
// 属性成员(Attributes)
public:
	TYPE* GetData()
	{
		TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject);
		return pData;
	}
	TYPE* GetDataNA()
	{
		TYPE* pData = (TYPE*)CThreadLocalObject::GetDataNA();
		return pData;
	}
	operator TYPE*()
		{ return GetData(); }
	TYPE* operator->()
		{ return GetData(); }

// 具体实现(Implementation)
public:
	static CNoTrackObject* CreateObject()
		{ return new TYPE; }
};


#define THREAD_LOCAL(class_name, ident_name) \
	CThreadLocal<class_name> ident_name;
#define EXTERN_THREAD_LOCAL(class_name, ident_name) \
	extern THREAD_LOCAL(class_name, ident_name)


#endif // __AFXTLS_H__

AFXTLS.CPP

//AFXTLS.CPP文件
#include "_AFXTLS_.H"

void CSimpleList::AddHead(void* p)
{
	*GetNextPtr(p) = m_pHead;
	m_pHead = p;
}

BOOL CSimpleList::Remove(void* p)
{
	if(p == NULL)	//检查参数
		return FALSE;
	
	BOOL bResult = FALSE; //假设移除失败
	if(p == m_pHead)
	{
	//要移除头元素
		m_pHead = *GetNextPtr(p);
		bResult = TRUE;
	}
	else
	{
		//试图在表中查找要移除的元素
		void* pTest = m_pHead;
		while(pTest != NULL && *GetNextPtr(pTest) != p)
			pTest = *GetNextPtr(pTest);

		//如果找到,就将元素移除
		if(pTest != NULL)
		{
			*GetNextPtr(pTest) = *GetNextPtr(p);
			bResult = TRUE;
		}
	}
	return bResult;
}

//-------------------CThreadSlotData类----------------------//
BYTE __afxThreadData[sizeof(CThreadSlotData)];	//为下面的_afxThreadData变量提供内存
CThreadSlotData* _afxThreadData; //定义全局变量_afxThreadData来为全局变量分配空间

struct CSlotData
{
	DWORD dwFlags;	//槽的使用标志(被分配/未被分配)
	HINSTANCE hInst;//占用此槽的模块句柄
};

struct CThreadData : public CNoTrackObject
{
	CThreadData* pNext; //CSimpleList类要使用此成员
	int nCount;	    //数组元素的个数
	LPVOID* pData;      //数组的首地址
};

#define SLOT_USED 0x01		//CSlotData结构中dwFlags成员的值为0x01时表示该槽已被使用

CThreadSlotData::CThreadSlotData()
{
	m_list.Construct(offsetof(CThreadData, pNext)); //初始化CTypedSimpleList对象

	m_nMax = 0;
	m_nAlloc = 0;
	m_nRover = 1;	//我们假定Slot1还未被分配(第一个槽(Slot0)总是保留下来不被使用)
	m_pSlotData = NULL;

	m_tlsIndex = ::TlsAlloc();	//使用系统的TLS申请一个索引
	::InitializeCriticalSection(&m_cs);	//初始化关键段变量
}

int CThreadSlotData::AllocSlot()
{
	::EnterCriticalSection(&m_cs);	//进入临界区(也叫关键段)
	int nAlloc = m_nAlloc;
	int nSlot = m_nRover;

	if(nSlot >= nAlloc || m_pSlotData[nSlot].dwFlags & SLOT_USED)
	{
		//搜索m_pSlotData,查找空槽(SLOT)
		for(nSlot = 1; nSlot < nAlloc && m_pSlotData[nSlot].dwFlags & SLOT_USED; nSlot ++) ;

		//如果不存在空槽,申请更多的空间
		if(nSlot >= nAlloc)
		{
			//增加全局数组的大小,分配或再分配内存以创建新槽
			int nNewAlloc = nAlloc + 32;

			HGLOBAL hSlotData;
			if(m_pSlotData == NULL)	//第一次使用
			{
				hSlotData = ::GlobalAlloc(GMEM_MOVEABLE, nNewAlloc*sizeof(CSlotData));
			}
			else
			{
				hSlotData = ::GlobalHandle(m_pSlotData);
				::GlobalUnlock(hSlotData);
				hSlotData = ::GlobalReAlloc(hSlotData, 
					nNewAlloc*sizeof(CSlotData), GMEM_MOVEABLE);
			}
			CSlotData* pSlotData = (CSlotData*)::GlobalLock(hSlotData);
	
			//将新申请的空间初始化为0
			memset(pSlotData + m_nAlloc, 0, (nNewAlloc - nAlloc)*sizeof(CSlotData));
			m_nAlloc = nNewAlloc;
			m_pSlotData = pSlotData;
		}
	}

	//调整m_nMax的值,以便为各线程的私有数据分配内存
	if(nSlot >= m_nMax)
		m_nMax = nSlot + 1;

	m_pSlotData[nSlot].dwFlags |= SLOT_USED;
	//更新m_nRover的值(我们假设下一个槽未被使用)
	m_nRover = nSlot + 1;

	::LeaveCriticalSection(&m_cs);
	return nSlot; //返回的槽号可以被FreeSlot, GetThreadValue, SetValue函数使用了
}

void CThreadSlotData::FreeSlot(int nSlot)
{
	::EnterCriticalSection(&m_cs);	

	//删除所有线程中的数据
	CThreadData* pData = m_list;
	while(pData != NULL)
	{
		if(nSlot < pData->nCount)
		{
			delete (CNoTrackObject*)pData->pData[nSlot];
			pData->pData[nSlot] = NULL;
		}
		pData = pData->pNext;
	}

	//将此槽号标识为未被使用
	m_pSlotData[nSlot].dwFlags &= ~SLOT_USED;
	::LeaveCriticalSection(&m_cs);
}

inline void* CThreadSlotData::GetThreadValue(int nSlot)
{
	CThreadData* pData = (CThreadData*)::TlsGetValue(m_tlsIndex);
	if(pData == NULL || nSlot >= pData->nCount)
		return NULL;
	return pData->pData[nSlot];
}

void CThreadSlotData::SetValue(int nSlot, void* pValue)
{
	//通过TLS索引得到我们为线程安排的私有存储空间
	CThreadData* pData = (CThreadData*)::TlsGetValue(m_tlsIndex);

	//为线程私有数据申请内存空间
	if((pData == NULL || nSlot >= pData->nCount) && pValue != NULL)
	{
		//pData的值为空,表示该线程第一次访问线程私有数据
		if(pData == NULL)
		{
			pData = new CThreadData;
			pData->nCount = 0;
			pData->pData = NULL;

			//将新申请的内存的地址添加到全局列表中
			::EnterCriticalSection(&m_cs);
			m_list.AddHead(pData);
			::LeaveCriticalSection(&m_cs);
		}

		//pData->pData指向真正的线程私有数据,下面的代码将私有数据占用的空间增长到m_nMax指定的大小
		if(pData->pData == NULL)
			pData->pData = (void**)::GlobalAlloc(LMEM_FIXED, m_nMax*sizeof(LPVOID));
		else
			pData->pData = (void**)::GlobalReAlloc(pData->pData, m_nMax*sizeof(LPVOID), LMEM_MOVEABLE);
		
		//将新申请的内存初始话为0
		memset(pData->pData + pData->nCount, 0, 
			(m_nMax - pData->nCount) * sizeof(LPVOID));
		pData->nCount = m_nMax;
		::TlsSetValue(m_tlsIndex, pData);
	}

	//设置线程私有数据的值
	pData->pData[nSlot] = pValue;
}

void CThreadSlotData::DeleteValues(HINSTANCE hInst, BOOL bAll)
{
	::EnterCriticalSection(&m_cs);
	if(!bAll)
	{
		//仅仅删除当前线程的线程局部存储占用的空间
		CThreadData* pData = (CThreadData*)::TlsGetValue(m_tlsIndex);
		if(pData != NULL)
			DeleteValues(pData, hInst);
	}
	else
	{
		//删除所有线程的线程局部存储占用的空间
		CThreadData* pData = m_list.GetHead();
		while(pData != NULL)
		{
			CThreadData* pNextData = pData->pNext;
			DeleteValues(pData, hInst);
			pData = pNextData;
		}
	}
	::LeaveCriticalSection(&m_cs);
}

void CThreadSlotData::DeleteValues(CThreadData* pData, HINSTANCE hInst)
{
	//释放表中的每一个元素
	BOOL bDelete = TRUE;
	for(int i=1; i<pData->nCount; i++)
	{
		if(hInst == NULL || m_pSlotData[i].hInst == hInst)
		{
			//hInst匹配,删除数据
			delete (CNoTrackObject*)pData->pData[i];
			pData->pData[i] = NULL;
		}
		else
		{
			//还有其它模块在使用,不要删除数据
			if(pData->pData[i] != NULL)
			bDelete = FALSE;
		}
	}

	if(bDelete)
	{
		//从列表中移除
		::EnterCriticalSection(&m_cs);
		m_list.Remove(pData);
		::LeaveCriticalSection(&m_cs);
		::LocalFree(pData->pData);
		delete pData;

		//清除TLS索引,防止重用
		::TlsSetValue(m_tlsIndex, NULL);
	}
}

CThreadSlotData::~CThreadSlotData()
{
	CThreadData *pData = m_list;
	while(pData != NULL)
	{
		CThreadData* pDataNext = pData->pNext;
		DeleteValues(pData, NULL);
		pData = pData->pNext;
	}

	if(m_tlsIndex != (DWORD)-1)
		::TlsFree(m_tlsIndex);

	if(m_pSlotData != NULL)
	{
		HGLOBAL hSlotData = ::GlobalHandle(m_pSlotData);
		::GlobalUnlock(hSlotData);
		::GlobalFree(m_pSlotData);
	}

	::DeleteCriticalSection(&m_cs);
}

//---------------------------------------

void* CNoTrackObject::operator new(size_t nSize)
{
	//申请一块带有GMEM_FIXED和GMEM_ZEROINIT标志的内存
	void* p = ::GlobalAlloc(GPTR, nSize);
	return p;
}

void CNoTrackObject::operator delete(void* p)
{
	if(p != NULL)
		::GlobalFree(p);
}

//----------------------------CThreadLocalObject 类--------------------------------//

CNoTrackObject* CThreadLocalObject::GetData(CNoTrackObject* (*pfnCreateObject)())
{
	if(m_nSlot == 0)
	{
		if(_afxThreadData == NULL)
			_afxThreadData = new(__afxThreadData) CThreadSlotData;
		m_nSlot = _afxThreadData->AllocSlot();
	}
 
	CNoTrackObject* pValue = (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);
	if(pValue == NULL)
	{
		//创建一个数据项
		pValue = (*pfnCreateObject)();

		//使用线程私有数据保存新创建的对象
		_afxThreadData->SetValue(m_nSlot, pValue);	
	}
	
	return pValue;
}

CNoTrackObject* CThreadLocalObject::GetDataNA()
{
	if(m_nSlot == 0 || _afxThreadData == 0)
		return NULL;
	return (CNoTrackObject*)_afxThreadData->GetThreadValue(m_nSlot);
}

CThreadLocalObject::~CThreadLocalObject()
{
	if(m_nSlot != 0 && _afxThreadData != NULL)
		_afxThreadData->FreeSlot(m_nSlot);
	m_nSlot = 0;
}

3.3设计自己的线程局部存储

上一篇:CMatrix类设计与实现(C++第一次实验)


下一篇:numpy和Pytorch对应的数据类型