MFC线程独立对象管理机制分析

来源:互联网 发布:人工智能女机器人电影 编辑:程序博客网 时间:2024/06/06 01:48

MFC线程独立对象管理机制分析

摘要

用一个对象名访问不同线程中不同的实例,而且这些实例属于同一个类,具有这种性质的对象称为线程独立对象(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_INLINEoperator TYPE*() // 间接调用基类的GetData方法。

         {return GetData(); }

 

     AFX_INLINE TYPE*operator->() // 间接调用基类的GetData方法。

         {return GetData(); }

 

public:

     //在局部堆创建客户对象。供CThreadLocalObject::GetData回调。

     static CNoTrackObject* AFXAPI CreateObject() {returnnew TYPE; }

};

 

源程序1   CThreadLocal模板类的方法

 

CThreadLocalObject的GetData方法取得SLOT单元中存放的客户对象指针,并返回。其中主要调用了CThreadSlotData类如下方法:

调用AllotSlot方法在SLOT数组中分配单元、调用GetThreadValue方法取对象指针、必要时创建客户实例,并调用SetValue方法保存新对象指针。

从_afxThreadState调用“->”方法开始,CThreadLocalObject::GetData的顺序图如图6所示。

    

     

 

 

 

CThreadLocalObject::GetData方法的流程图如图7所示。

 

CThreadLocalObject::GetData方法总是返回客户对象指针,如果属于线程的客户对象指针已经在SLOT单元,从SLOT数组的m_nSlot单元直接取出对象指针返回;如果客户对象还不存在,创建新的客户对象保存到SLOT单元,然后返回新对象指针。

 

应用进程第一次调用CThreadLocalObject::GetData方法时,_afxThreadData变量保存了新创建的CThreadSlotData实例。MFC系统只设一个全局的_afxThreadData实例变量。

一个CThreadLocalObject封装一个客户类。它把一个客户类转换为线程独立类,它为客户类申请一个SLOT单元号,存放在m_nSlot属性中。线程独立对象第一次调用GetData时,申请到一个SLOT号,这个SLOT号将用于该类的所有实例。

调用GetData时,只要SLOT单元保存的指针是空值,就创建新客户对象,新客户对象指针保存到活动线程的SLOT单元,并返回新创建的对象指针。

因为线程运行的独立性,只有活动线程执行GetData后,才保证线程的SLOT单元中存放着客户对象指针。

 

 

4、实现:CThreadSlotData构造的数据结构和方法

经过精心设计,CThreadSlotData足以为整个MFC系统提供服务。它构造了伸缩自若的动态数据结构,实行高效有序的管理方法。它向CThreadLocalObject开放了功能调用,把所有管理细节封装在了类的内部。

 

(1)数据结构

数据结构分为:SLOT数组,管理SLOT数组的辅助结构。辅助结构有两部分,分为SLOT状态数组,SLOT数组头的链结构。

数据结构如图8所示:

 

 

 

m_tlsIndex属性:MFC线程独立对象管理子系统只占用一个TLS槽口,申请得到的TLS槽口号保存在m_tlsIndex属性中。

m_nAlloc属性:记录了SLOT状态数组的大小。

m_nMax属性:记录了SLOT状态数组中被占用过的最大单元序号。m_nMax <= m_nAlloc关系始终有效。

m_nRover属性:它等于最近分配的SLOT序号的下一个,因为下一个SLOT序号是最有可能空闲的。m_nRover属性帮助在SLOT状态数组快速查找空闲的单元。

m_list链表:以CThreadData作为节点构成的链表,每个节点记录了一个线程的SLOT数组指针和SLOT数组项数。每个线程的TLS槽口保存一个CThreadData实例指针。

m_pSlotData表:以CSlotData作为数据项构成的动态线性表。dwFlags标志记录了每个SLOT单元的空闲和占用状态,hInst记录了客户对象所属的模块。SLOT状态数组每次以32单位扩展。

SLOT数组:保存客户对象指针。每个线程都有自己的SLOT数组。同一个SLOT号的单元保存一个客户类在不同线程中的不同实例指针。SLOT数组不够大时,直接扩展到m_nMax个单元。

 

(2)方法:与数据结构配合管理SLOT单元,存取客户对象指针

 

对SLOT状态数组的操作:寻找dwFlags等于空闲(!SLOT_USED)的单元,若所有SLOT单元被占用,一次扩展32个空闲单元。

对m_list链表的操作:线程第一次保存客户对象时,创建CThreadData实例并插入m_list表头。SLOT数组变化后更新CThreadData实例的属性值。在Windows TLS槽口中读取和存入CThreadData实例指针。

对SLOT数组的操作:从指定单元取客户对象指针。将客户对象指针存入指定序号的单元中,若SLOT数组不够大,将SLOT数组扩展到m_nMax个单元。

 

(a)CThreadSlotData构造方法

初始化对象的属性值。MFC线程独立对象管理子系统只占用一个TLS槽口,构造程序向Windows 申请TLS槽口,得到的TLS槽口号保存在m_tlsIndex属性。

 

(b)AllocSlot方法

分配SLOT单元。在m_pSlotData数组寻找dwFlags等于空闲(!SLOT_USED)的单元。

若整个进程第一次调用AllocSlot,新建32个单元的SLOT状态数组。

若SLOT单元全部占用,一次扩展SLOT状态数组32个单元。

返回一个空闲的单元号并设置该SLOT单元为占用。

AllocSlot方法的流程图如图9 所示:

 

 

 

 


(c)SetValue方法

在SLOT数组nSlot单元保存客户对象指针,pValue是客户对象指针,nSlot是预先分配给线程独立类的SLOT单元号。SetValue方法的流程图如图10 所示。

线程第一次保存客户对象时,创建CThreadData实例,在TLS槽保存CThreadData实例指针,并插入m_list链表头部。然后创建m_nMax个单元的SLOT数组,CThreadData实例的pData属性保存SLOT数组指针。

如果SLOT数组太小,重新将SLOT数组扩展到m_nMax个单元。

m_nMax是SLOT状态数组中曾经分配过的最大单元号。

客户对象指针将存放在预先分配的SLOT单元中。

 

 

 

(d)GetThreadValue方法

从SLOT数组nSlot单元取客户对象指针,返回对象指针。GetThreadValue方法的流程图如图11 所示。

首先从TLS槽取CThreadData实例指针,然后从pData得到SLOT数组指针,返回nSlot单元中保存的客户对象指针。

如果线程从来没有保存过任何对象指针,那么从TLS槽取出的是空指针NULL;如果线程没有保存过这个客户对象指针,那么从SLOT数组取出的是空指针NULL。

 

 

 

 

(e)FreeSlot方法

释放SLOT单元。删除所有线程中SLOT数组nSlot单元中保存的客户对象。在SLOT状态数组nSlot单元记录空闲标志。FreeSlot方法的流程图如图12 所示。

 

 

 

(f)CThreadSlotData析构方法

沿着m_list链表删除每个线程的每一个客户对象。释放每个线程的SLOT数组存储区、删除m_list链表的每个节点、释放SLOT状态数组存储区。释放TLS槽口。

 

四、应用

还是以AFX_THREAD_STATE结构为例。MFC定义了宏:

#define EXTERN_THREAD_LOCAL(class_name, ident_name) /

    extern CThreadLocal<class_name> ident_name;

 

实例化_AFX_THREAD_STATE线程独立对象代码在MFC源码中写为:

class _AFX_THREAD_STATE : public CNoTrackObject{......};

EXTERN_THREAD_LOCAL(_AFX_THREAD_STATE, _afxThreadState)

 

运行时,各个线程中存在的_AFX_THREAD_STATE实例如图13所示。

CThreadData对象的m_tlsIndex属性保存着申请得到的TLS槽口号。

线程独立对象_afxThreadState的m_nSlot属性保存着申请得到的SLOT单元号。每个线程的SLOT数组的m_nSlot单元可以存取属于线程的_AFX_THREAD_STATE实例的指针。

 

 

   

MFC系统中定义的线程独立对象主要有:

THREAD_LOCAL(_AFX_THREAD_STATE, _afxThreadState)

THREAD_LOCAL(_AFXCTL_AMBIENT_CACHE, _afxAmbientCache)

CThreadLocal<AFX_MODULE_THREAD_STATE> m_thread

 

原创粉丝点击