Windows MFC 工程应用开发与框架原理完全剖析教程(中)之上部
- 第3章 原理篇二 MFC核心框架完全实现
- 3.1 MFC顶层类结构设计与RTTI、CRunTimeClass设计思想剖析
- 3.2 RTTI设计与验证
- 3.3 动态类型识别技术:DyNamic的设计与实现
- 3.4 动态创建技术——Declare宏
- 3.5 CWinThread的深入剖析与实现(1):CSimpleList的实现
- 3.6 CWinThread的深入剖析与实现(2):MFC工作者线程的设计-CTypedSimpleList与CSimpleList的泛型化实现
- 3.7 CWinThread的深入剖析与实现(3):MFC工作者线程的设计-CNoTrackObject的实现
- 3.8 CWinThread的深入剖析与实现(4):MFC工作者线程的设计-MFC对线程推进的封装基石-Windows的TLS机制详解
- 3.9 CWinThread的深入剖析与实现(5):MFC工作者线程的设计-MFC封装TLS的原理剖析
- 3.10 MFC工作者线程的设计——MFC封装TLS的头文件编码实现
- 3.11 MFC工作者线程的设计——MFC封装TLS 1
- 3.12 MFC工作者线程的设计——MFC封装TLS2
- 3.13 CThreadSlotData的析构、DeletValues与CThreadLocalObject函数的编码实现
- 3.14 MFC工作者线程的设计——MFC封装TLS的单元测试与总结
- 3.15 MFC工作者线程的设计——CWinThread的完整实现
- 3.16 MFC工作者线程的设计——CWinThread的单元测试与Bug解除
- 3.17 MFC工作者线程的设计——CWinThread流程运行总结
第3章 原理篇二 MFC核心框架完全实现
3.1 MFC顶层类结构设计与RTTI、CRunTimeClass设计思想剖析
























//动态确定类
class CAge:public CObject
BOOL IsAge(CObject* pO)
{
return pO->IsKindOf( RUNTIME_CLASS( CAge ) );
}
BOOL IsAge2(CAge* pO)
{
return pO->IsKindOf( RUNTIME_CLASS( CAge ) );
}
void main(void)
{
CObject a;
CAge b;
IsAge(&a);//return FALSE
IsAge(&b);//return TRUE
IsAge2((CAge*)&a);//return FALSE,避免强制转换带来的错误
}
CObject CRuntimeClass::CreateObject(void)可以产生一个类变量。作用和new类似,但在某些特殊场合有独特的作用。
//生成类
class CWndA: public CWnd
class CWndB: public CWnd
function1()
{
CRuntimeClass* pC=RUNTIME_CLASS( CWndA );
CreateWnd(pC);
}
CWnd* CreateWnd(CRuntimeClass* pClass)
{
return (CWnd*)pClass->CreateObject();
//相当于classCWndA->CreateObject();
}
在 上面例子中,CreateWnd返回的是CWnd* 其实它是一个CWndA*。你可以进行由父类到子类的强制转换而不必要担心出错。
使用CRuntimeClass可以代替使用switch生产类实例的一 些繁琐。(请好好想想它的用途,当你发现它的好处时,你一定会大吃一惊,M$使用宏来实现类的动态检测,如果谁有兴趣可以去看看MFC的源代码。)
注意:在类的定义中使用IMPLEMENT_DYNCREATE后方可生效。
MFC中经常只需要构建类,程序就可以运行了,并没有看到创建对象的过程。那么对象是如何创建的呢?
这就是动态创建机制。
动态创建允许,MFC自动为你编写的类创建对象,因此,在编写代码过程中只需进行类的编辑。
为了实现自动创建对象必须在每个类中建立一个结构体CRuntimeClass,这个结构体主要记录了类的几个信息:类名,几个指针,以及构建函数CreateObject。
指针用于把程序中所有的类(包括你创建的和MFC自动创建的类)串联起来,在整个程序中建立一张类的链表。需要指出,这种串联既是横向也是纵向的。横向是指,每一个类都会按照构建次序连起来(pfirst 和pnext的串联)(通过m_pNextClass,将所有CRuntimeClass对象用简单链表连在一起)。纵向是指,结构体中还有一个指针m_pBaseClass,指向该类的父类。这是为了方便查询出类的派生关系。
构建函数,用于构造类的对象。能动态创建的类中有CreateObject()函数。
能动态创建的类的CRuntimeClass结构中的m_pfnCreateObject字段初赋值为CreateObjec,如果为NULL,则不具备动态创建的能力。
上述构建函数可以参考:https://www.cnblogs.com/tinaluo/p/7826678.html
有了这些准备,动态创建就很容易了。例如要求在主程序中,提供一个类名,创建该类的对象。那么只需要编写一个搜索函数,有了类的链表,搜索函数很容易搜索出所对应的类,搜索成功后,调用该类中的构建函数,就可以产生对象。
上述搜索函数可以参考:https://blog.youkuaiyun.com/armman/article/details/1527409
3.2 RTTI设计与验证






上图return pClassThis->IsDerivedFrom函数的参数错了,应由pClassThis改为pClass。

上图RUNTIME_CLASS的宏定义,其中的CRuntimClass应改为CRuntimeClass。







3.3 动态类型识别技术:DyNamic的设计与实现












3.4 动态创建技术——Declare宏

上图提示错误,我们通过下图的设置输出预处理文件,来检查宏展开后的代码语句。



可以看到上图最后面有一个转义的反斜杠,应该把它去掉。












3.5 CWinThread的深入剖析与实现(1):CSimpleList的实现



















3.6 CWinThread的深入剖析与实现(2):MFC工作者线程的设计-CTypedSimpleList与CSimpleList的泛型化实现


上图有误,应改为:
TYPE GetNext(TYPE p){
return (TYPE)CSimpleList::GetNext(p);
}






3.7 CWinThread的深入剖析与实现(3):MFC工作者线程的设计-CNoTrackObject的实现









如果我们的数据节点不从CNoTrackObject继承的话:


它调用的是通用的new和delete操作符。
3.8 CWinThread的深入剖析与实现(4):MFC工作者线程的设计-MFC对线程推进的封装基石-Windows的TLS机制详解


这是我们原先的一个非常简单的工作者线程的使用,我们在main函数当中直接用AfxBeginThread就推进出来了,根据微软的讲法,在MFC当中我们不需要去CloseHandle,也不需要去做一系列的线程的关闭,我们直接调这样的参数就可以使用起来了,不需要我们去删除对象,不需要我们去管理句柄。

对于线程局部存储,我们得注重两个概念:
(1)线程局部存储的位数组;
(2)线程与位数组的对应。








3.9 CWinThread的深入剖析与实现(5):MFC工作者线程的设计-MFC封装TLS的原理剖析

我们希望线程和线程之间的数据也可以有条理的访问:

MFC利用线程局部存储进行一种扩展,使得我们原先有固定数量的线程局部存储,变得可以无限扩展了。

3.10 MFC工作者线程的设计——MFC封装TLS的头文件编码实现




我们定义一个CThreadLocalObject类,这个是我们为了完成槽的分配而定义的一个辅助数据结构,即槽中数据所指的真正的用户数据。

这个实际上是我们线程局部存储当中那个槽里面的对象,所以肯定有一个槽号。
我们仿照MFC里面的内容,对CThreadLocalObject进行模板化:


在我们开发过程当中,为什么MFC在做这个工作者线程的时候,它不允许非MFC对象跨线程来访问,因为一旦跨线程访问的话就脱离我们这个TLS的管理,使得你的这样一个实现超越了我们链表的管理,使得你的内存管理不可控,因为你也知道它的内存都已经不在C++运行时了,实际上是向操作系统要的,所以它不能跨线程的访问。
3.11 MFC工作者线程的设计——MFC封装TLS 1






3.12 MFC工作者线程的设计——MFC封装TLS2





3.13 CThreadSlotData的析构、DeletValues与CThreadLocalObject函数的编码实现








3.14 MFC工作者线程的设计——MFC封装TLS的单元测试与总结










在我们调用的过程当中,已经非常像MFC的CWinThread的入口点函数AfxThreadEntry了,换句话说,我们只要把上图78行到81行这几行给去掉,变成一个AfxThreadEntry函数调用就OK了,但是我们现在还没有对_bgeinthreadex和WaitForMultipleObjects,以及CloseHandle来做一些事情(要把它们封装起来),我们还不能自动关闭这里面的内容(线程句柄、同步事件、线程局部对象等),所以接下来我们就开始编码CWinThread工作者线程。
3.15 MFC工作者线程的设计——CWinThread的完整实现
新建定义CWinThread类的头文件_AFXWIN.h:


这个m_hThread是说,既然CWinThread是一个C++的语法,它里头应该能保存一个句柄,这个句柄才是真正调用Windows的SDK帮我们把这个thread创建出来;
重载一个小括号运算符,它的实现就是return一个m_hThread。

在我们这一个进程当中可能会有多个线程,每个线程当中呢会有各种各样的模块放在里面(线程私有数据),在这些模块当中对于我们这一个线程来讲它是私有的,比如说线程1有自己的模块,线程2也有自己的模块,这些模块我们必须作为线程得维护它的状态,因为这些可执行体(模块)它们也会被推进,推进到哪里也会被记录,那这个时候我们得再设计一个类来记录这样的状态,我们把这样的状态叫AFX_MODULE_THREAD_STATE,很明显这个线程状态必须依赖于TLS。

换句话说,我们的MFC内部就维护了每一个线程独立的每一个模块;
我们还得把它进行维护,让它能够用链表把它串起来,以便管理各线程的状态,使用THREAD_LOCAL宏把表示线程状态的全局变量_afxModuleThreadState改写成线程局部变量(通过我们自己的线程局部存储),即CThreadLocal<AFX_MODULE_THREAD_STATE> _afxModuleThreadState。


我们除了AFX_MODULE_THREAD_STATE的定义,我们还得想一想,我们用户使用CWinThread创建线程的时候啊,除了设置线程的状态,进入消息循环,我们还要把线程的函数地址进行适当地设置,但是我们又不能直接的暴露给用户,那这个时候呢我们就想了,我们要有一个设计来帮助我们把Windows SDK里面的C++函数进行一个设计。

AFX_THREADPROC这个类型的函数将用来初始化我们的线程参数,帮助我们线程的运行。


我们回想一下MFC的使用过程,需要我们提供一个AfxThreadEntry来帮助我们进入函数的入口点,那接下来我们实现AfxThreadEntry,新建CWinThread类的实现文件ThreadCore.cpp:

也就是说,我需要为子线程创造了一定的环境以后(例如同步机制),才能把这个子线程进行推进,所以这个时候我们还得有一个封装,叫AFX_THREAD_STARTUP。

我们通过这样一个结构,把它放进Win32的CreateThread函数里面去,可以这么讲,我们是把CWinThread组合进入CreateThread,来完成CWinThread的入口点函数AfxThreadEntry的。
这两个线程(父线程和子线程)的交互用的是事件对象:

第1个hEvent告诉你在子线程创建了以后触发,告诉父线程我们已经创建好了,告诉父线程你可以推进了;
第2个hEvent2是说,接下来当我这个调用线程所有的分量全部准备完毕以后再触发,换句话说,就是告诉子线程你可以推进了。
实现线程的回调函数_AfxThreadEntry:

这个时候呢,既然有了AFX_THREAD_STARTUP,我们就可以为CWinThread对象组合进来提供帮助了。
通过AFX_THREAD_STARTUP获得我们将要推进的CWinThread对象,即pStatup->pThread
接下来我们要对线程的状态进行设置,看看里面的内容:

看看我们封装出来的这个对象能否获得当前的这个模块的状态,同时我们把当前的状态进行一个指定,指向当前的pThread这个CWinThread对象;
通过try…catch语句看看这个转换是否OK,有没有异常,因为线程当中有可能会被其他打断,这个时候我们就不能往下执行了,设置bError为TRUE,告诉父线程新创建的这个子线程初始化出错。


CommonConstruct函数它的任务是要构建我们的对象。
除此之外,我们还要给用户提供两个全局函数:



我们回到ThreadCore.cpp里面,上图这个地方,当父线程作为一个入口点去推进的时候,我们会考虑,这个pStartup指向的内存空间是咱们的新线程的空间,当我们启用了新线程的初始化之后啊,有可能当新线程的堆栈如果构造不成功的话,可能会被侵蚀掉这个线程的空间(pStartup指向的内存空间),所以我们要在CreateThread能够正常初始化之前呢,我们先保存下pStartup->hEvent2的值,以保证线程初始化是完备的(避免子线程死锁)。



要返回当前线程CWinThread对象的指针,我们就得拿它的状态。

也就是说AFX_MODULE_THREAD_STATE这个对象始终封装了我们CWinThread对象。

这是我们自己写的线程局部存储,既然线程CWinThread对象都不在了,那么我们也要把我们自己分配的内存空间给释放,不然的话会占用内存资源的;
参数FALSE表明仅仅删除当前线程的线程局部存储 占用的空间。

vs智能感知不到_afxThreadData变量,它是放在AFXTLS.cpp文件当中的,所以需要调整一下位置,放到_AFXSTAT.h里面:


我们之所以创建线程的时候让它先挂起,是要考虑父线程和子线程的交互,并且让子线程有足够的时间,充分的去加载它里面的资源。






如果要是创建成功,这个线程句柄存在了,那我们要恢复线程的执行。
讲白了,就是告诉你父线程把你这个子线程创建出来了以后,先让你把里面的空间全部加载好,然后接下来再推进你。
这个时候在ResumeThread之后,我们就要开始让子线程去把相应的空间初始化完毕,所以要等待你(子线程)告诉我初始化完毕好了,告诉我什么时候能够推进,由你子线程完成告诉我。

如果用户一上来并不是要求你直接推进,你创建的是一个挂起的线程,我就先让你暂时停止推进,等什么时候需要推进的时候由你来推进。
用户是可以创建一个挂起的线程,这是一个常见的动作。
线程创建是有可能失败的,因为资源不充分。


3.16 MFC工作者线程的设计——CWinThread的单元测试与Bug解除

我们来看看期待已久的MFC工作者线程是怎么实现的。



你看,这个AfxBeginThread就跟MFC的工作者线程非常相近了,至此我们CWinThread基本工作内容就到这里了。

注意刚才我们贴错了,应该贴到_AFXTLS_.h文件中:



这个时候就会出现上图这种重定义的问题,这个原因是什么呢?
这个是因为重复包含导致的,我们为了解决这种重复包含,我们会对头文件进行修订,因为在C/C++当中再做混合编程的时候,特别要注意这个问题,因为C语言是没有所谓的命名空间的概念的,因此它会以文件为单位,而在我们的ThreadCore.cpp里面用到的_afxThreadData,和AFXTLS.cpp里面的_afxThreadData,就会有这种重复冲突的现象,所以在obj文件里面就没有办法有唯一的生成。



同样的,_AFXSTAT.h和_AFXWIN.h这两个头文件里面也要做同样的修正:





剪切上图选中的两行内容,放到AFXTLS.cpp文件中:




3.17 MFC工作者线程的设计——CWinThread流程运行总结
我们来看看工作者线程究竟做了什么事情。
我们从父线程创建开始来看:

新线程创建,那么这个时候新线程创建在我们操作系统来看啊,它实际上是把一个线程的堆栈和一些空间给你,但是它并没有立即执行,为什么呢,因为你要知道我们可能会把一些句柄啊,一些模块啊,还有一些相应的能够被Windows推进的执行体给放进来,在放进来的当中它没有完全初始化好的话,它是不能够被执行的,所以怎么办呢,它先挂起,这是子线程做的事情。

上图左侧是MFC在做的事情,MFC帮助我们推进这个新线程(右侧),所以这个新线程开始执行AfxThreadEntry。



这个就是我们CWinThread的流程,可以这么讲,这个CWinThread我们虽然用起来很方便,但是MFC为了推进我们这个应用程序做了大量的工作,大家一定要明白这里面的过程,所以我们的新线程并不是一上来就会被推进的。
现在基本上可以知道两点,第一个,CWinThread之所以能够封装WinMain这个函数,也是因为可以把它当成一个函数执行体,作为线程这样一个指令序列的聚合体进行推进,所以我们的MFC借助CWinThread把WinMain函数封装起来;
第二个,我们MFC在做这个线程推进的时候,它考虑到了这种线程交互的状态,所以做了一系列的内容,并且能够自己管理内存,可以管理线程自有数据,并且同时通过了TLS这样的技术,使得线程的交互变得更加的统一和协调,在这个过程当中,可以说它是一个比较复杂的运用了Windows操作系统底层的机理。
6197

被折叠的 条评论
为什么被折叠?



