摘要
用一个对象名访问不同线程中不同的实例,而且这些实例属于同一个类,具有这种性质的对象称为线程独立对象(Thread Isolated Object)。
MFC系统的核心部分提供了线程独立对象的支持。
本文使用UML、流程图、数据结构图示详细分析了线程独立对象的支持子系统的实现方法。主要分析了三个类CThreadLocal 模板类、CThreadLocalObject类、CThreadSlotData类的分工和关系,另外主要分析了两个结构体CThreadData、CSlotData构成的数据结构。
感兴趣于MFC系统源码和MFC深层机制的人们一定会从文章受到启发。
关键词:线程独立对象,TLS,MFC
一、概述
作者在编程工作中,对MFC系统源码产生了浓厚的探究愿望。希望借鉴MFC系统源码中Windows编程精髓的技术,以它山之石为我所用;另一方面也为了解除编程过程中的脚不踏地的困惑感觉。希望达到编写MFC程序时胸有成竹的境界。
本文是作者多年分析MFC系统源码的心得之一。纵观目前已经出版的MFC书籍,方方面面的文章已经很多了。受到 侯俊杰 先生《深入浅出MFC》一书的启发,潜行在MFC系统分析,发现还有许多技术精髓有待挖掘,其中之一就是源码中频繁出现的线程独立对象的有关代码。于是拿出勇气和毅力,经历困惑和豁朗,几易其稿,终于可以面世。希望本文能够抛砖引玉,促进MFC系统分析的活跃和繁荣。
使用MFC编程的人们都知道,有了MFC系统代码提供的服务,编写应用程序省却了许多常规性的编码工作,让我们更专心致志地编写与应用密切相关的代码。
MFC应用程序实际上由两部分组成,一部分是我们编写的代码,另外一部分是MFC系统代码,两者的关系可以理解为使用和提供服务的关系。正像图1所示:
我们知道程序具有静态的概念,线程具有动态概念。相同的程序在多个不同线程中运行,处理的数据不同。我们按照规范一致的模式编写程序,并保证运行后各个线程的数据互相隔离,希望实现线程运行环境的独立。
在应用程序运行过程中,MFC系统实施服务。以多线程方式运行的Windows应用程序,不同线程中线程独立对象被MFC系统分别管理,各个线程独立类的实例分配到不同的存储区。运行时的MFC应用系统如图2 所示:
概念上可以把MFC系统中支持线程独立对象的功能划分为子系统,所有其它的代码都作为这个子系统的客户,这个概念模型如图3 所示。子系统提供了一致透明的接口,取线程独立对象指针的操作被重新定义为取活动线程中保存的客户对象指针。子系统充分依靠Windows TLS的功能。
为了服务于整个MFC系统,子系统改善了存取客户对象指针的效率。而且创建SLOT数组,外延扩展了TLS槽的容量。任何子系统以外的代码都是客户,它们把对象指针保存在自己线程的SLOT单元中,每个SLOT单元是代客户设置的对象指针保管箱,子系统负责保管箱的分配、存入物品(客户的对象指针)、取出物品、回收保管箱。
线程独立对象变量和活动线程特征唯一确定了SLOT单元。
本文余下部分将分析MFC核心系统之一:MFC线程独立对象管理子系统。
二、准备知识:TLS
TLS(Thread Local Storage)是windows系统的资源。每个进程拥有一组TLS槽口,每个槽口用序号标识,Windows TLS API函数可以分配释放TLS槽口,在TLS槽口存取数据。进程中多个线程使用同一个TLS槽口,却可以保存线程独立的数据。讲得透彻点,线程标识和槽口号唯一确定了二维空间的存储单元。每个槽口单元保存4字节数据,可以保存整数、对象指针、数组的指针等。
根据windows系统版本不同,TLS槽口数量有所区别,最少是64个槽口,windows 2000的每个进程可以有1088个槽口。
使用之前进程向Windows系统申请一个槽口,系统根据槽口空闲情况返回一个可用的槽口号。各线程使用同一个槽口号读写线程自己的数据。
申请槽口号、在槽口存取数据、释放槽口等操作使用的Windows API函数简述如下。
TlsAlloc函数分配一个槽口号,该槽号可以被所有本进程的每个线程使用。TlsGetValue、TlsSetValue函数以分配的槽口号为参数,读写数据。TlsFree函数释放不用的TLS槽口号。
使用TLS的步骤:
1、进程初始化时分配TLS槽口;
DWORD gdwTlsIndex;
gdwTlsIndex = TlsAlloc();
2、调用TlsSetValue保存数据;
LPVOID lpvBuffer;
lpvBuffer = (LPVOID) LocalAlloc(LPTR, 256);
TlsSetValue(gdwTlsIndex, lpvBuffer); // 保存存储区指针。
3、调用TlsGetValue取数据;
LPVOID lpvData;
lpvData = TlsGetValue(gdwTlsIndex); // 取TLS槽口中保存的存储区指针。
4、调用TlsFree 释放槽口。
lpvBuffer = TlsGetValue(gdwTlsIndex);
LocalFree((HLOCAL) lpvBuffer); // 释放存储区
TlsFree(gdwTlsIndex); // 释放TLS槽口
无论哪一种windows系统版本,对一个进程而言TLS槽口数量总是有限的。为了在一个槽口中尽可能多地保存线程数据,每个线程都在局部堆中分配一块存储区,槽口中保存存储区的指针。TLS槽口中保存的指针可以是任何结构化的线程局部数据的存储区开始。
三、功能实现:管理线程独立对象
1、类图
MFC系统主要设计了三个类:CThreadLocal 模板类、CThreadLocalObject类、CThreadSlotData类,另外使用了两个结构体:CThreadData、CSlotData。以Windows TLS资源为基础,实现了线程独立对象的存取。
类图如图4和图5所示。
CThreadLocal 模板类与CThreadLocalObject类存在继承关系。CThreadLocalObject类使用CThreadSlotData类的方法。
CThreadLocal 模板类重新实现了运算符“->”和“*”方法,它们都将调用GetData方法,GetData方法又直接调用基类的GetData方法。CThreadLocal 模板类运算符“->”和“*”方法决定性地把取对象指针的操作转向到取TLS中保存的客户对象指针。
CThreadLocalObject类是CThreadLocal的基类,必要时它将创建TYPE类型的线程中的对象。CThreadLocal实现了线程独立对象特例,CThreadLocalObject类任何现成独立对象具有的泛化的功能。CThreadLocalObject类调用CThreadSlotData实例的方法,实现了为TYPE类型的对象申请SLOT号、在SLOT单元存取线程的客户对象指针。
CThreadSlotData类基于TLS资源,构建了组织完善的数据结构和管理方法,使用一个TLS槽口向MFC的客户线程提供一组SLOT单元的存取服务,严谨有序地管理SLOT单元和存取线程的客户对象指针。
2、CThreadLocal 模板类
任何类要具备线程独立性质,在MFC系统中必须符合如下两个条件:
(1)继承CNoTrackObject类;
(2)作为CThreadLocal 模板类TYPE的参数,实例化;
例如:MFC系统中频繁使用_AFX_THREAD_STATE结构,定义和实例化_AFX_THREAD_STATE对象的代码如下:
class _AFX_THREAD_STATE : public CNoTrackObject{......};
CThreadLocal<_AFX_THREAD_STATE> _afxThreadState;
_afxThreadState实例变量与各个线程中的_AFX_THREAD_STATE对象具有1:M(一对多)关系。_afxThreadState具有线程独立对象的性质,各个线程中_AFX_THREAD_STATE对象被称为子系统(线程独立对象管理子系统)的客户对象。
CThreadLocal 的算子方法“->”和“*”从TLS中取_AFX_THREAD_STATE实例指针。
调用_afxThreadState的“->”或“*”方法时,根据活动线程标识取得_AFX_THREAD_STATE实例指针。
_afxThreadState->属性名,将存取活动线程的客户实例(_AFX_THREAD_STATE实例)的属性值。
CNoTrackObject类是每一个希望成为线程独立的客户类都必须继承的基类, 它重写了new 和delete方法,重写后的new调用::LocalAlloc;重写后的delete调用::LocalFree。用new 创建的客户对象存放在局部堆存储区。
3、实现:GetData方法
从CThreadLocal 模板类的代码看到,我们可以重点分析CThreadLocalObject类的GetData方法。
template<class TYPE>
class CThreadLocal : public CThreadLocalObject
{
public:
AFX_INLINE TYPE* GetData(){ //调用基类的GetData方法。
TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject);
return pData; }
AFX_INLINE TYPE* GetDataNA(){ // 调用基类的GetDataNA方法。
TYPE* pData = (TYPE*)CThreadLocalObject::GetDataNA();
return pData; }
AFX_INLINE operator TYPE*() // 间接调用基类的GetData方法。
{ return GetData(); }
AFX_INLINE TYPE* operator->() // 间接调用基类的GetData方法。
{ return GetData(); }
public:
// 在局部堆创建客户对象。供CThreadLocalObject::GetData回调。
static CNoTrackObject* AFXAPI CreateObject() { return new TYPE; }
};
源程序1 CThreadLocal模板类的方法
CThreadLocalObject的GetData方法取得SLOT单元中存放的客户对象指针,并返回。其中主要调用了CThreadSlotData类如下方法:
调用AllotSlot方法在SLOT数组中分配单元、调用GetThreadValue方法取对象指针、必要时创建客户实例,并调用SetValue方法保存新对象指针。
从_afxThreadState调用“->”方法开始,CThreadLocalObject::GetData的顺序图如图6所示。
CThreadLocalObject::GetData方法的流程图如图7所示。