2008年11月22日星期六

设计模式step_by_step(序)

学习是一件很简单的事情,只要有了合适的学习对象、环境(我们可以称其为资源)再加上持续的独立思考便慢慢会有成效。孟母三迁也是为了给孩子好的学习环境。中小学时有老师帮我们搜集整理资源,对于我来说,学习的过程非常顺利。进入大学后,这个搜集整理工作无人帮忙了,便放缓了学习的脚步近乎停顿。另外,面对知识的海洋,在“学什么”这个问题上也变得不知取舍、手足无措。

当VC用到一年、但始终停留在混乱的面向过程和面向对象混用的门外汉阶段时,我问自己有没有书教我们如何在一个具体的项目中使用面向对象思想,找了一些书却没有结果。于是继续拼拼凑凑的写着可怜的代码,没有提高。今天,突然看到武大计算机学院k_eckel写的关于设计模式的读书笔记,才知道原来我一直寻找的面向对象思想早就被前人总结出来,甚至在前些年风靡一时。当得知此事时,我不得不汗颜自己的编程实践太少,所以才进步缓慢。

下面摘抄一小段《设计模式:可复用面向对象软件基础》中的话(顺序有变):
本书中的设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。
它并不描述链表和hash表那样的设计,尽管它们可以用类来封装,也可以复用;也不包括那些复杂的、特定领域内的对整个应用或子系统的设计。书中讨论的设计模式仅仅包含了一个设计行家所知道的部分。没有讨论与并发或分布式或实时程序设计有关的模式。本书并不准备告诉你怎样构造用户界面、怎样写设备驱动程序或怎样使用面向对象数据库,这些方面都有自己的模式,将这些模式分类编目也是件很有意义的事。

如此看来,设计模式非常有局限性,但是(下面是k_eckel的话)“设计模式体现的是一种思想,而思想则是指导行为的一切,理解和掌握了设计模式,并不是说记住了23种(或更多)设计场景和解决策略(实际上这也是很重要的一笔财富),实际接受的是一种思想的熏陶和洗礼,等这种思想融入到了你的思想中后,你就会不自觉地使用这种思想去进行你的设计和开发,这一切才是最重要的。”

当然,在学习一个优秀成果的同时也认识到它的局限性是非常有必要的,上面列出的一系列局限也警示着不足、指引着拓展的方向。

2008年11月10日星期一

结束工作者线程的方法--多线程编程

基础:

1. Windows线程如何结束?

安全方法:ExitThread
强制方法:TerminateThread

共性:
线程结束时都会将线程状态设置为signaled,线程结束状态由STILL_ACTIVE修改为退出码(dwExitCode)。
个性:
安全方法将在结束线程时同时释放线程的栈和线程连接的DLL;
强制方法不进行上述清理工作,是危险的方法,不推荐使用,并且需要与THREAD_TERMINATE标志联合使用。

2. 工作者线程如何结束?

工作者线程是MFC特有的名词,它是MFC将普通Windows线程进行包装后的结果,所以最终工作者线程的结束也要归结到1中提到的两种方法。线程结束工作由全局函数AfxEndThread完成,控制函数执行完毕后,框架会自动调用该函数结束线程。或者在控制函数的执行过程中显示的调用该函数结束线程,调用者必须是待结束的线程本身。

3. 工作者线程对象合适删除?

工作者线程对象的删除是MFC份内或称其为编译器级的工作,而工作者线程的结束归根结底是操作系统完成的工作,两者有区别。

CWinThread类有一成员变量m_bAutoDelete,若该变量为“TRUE”时,AfxEndThread将调用CWinThread::Delete删除线程对象;相反,设为“FALSE”不会删除,需要编程者手动删除。

实战:

1. 什么时候不设置为自动删除线程对象?

线程结束后,可能还有一些与线程有关的句柄(它们映射的对象是线程对象的成员变量)未关闭以进行额外的工作,如同步,此时需要根据程序需要手动删除对象。

2. 用户如何通知线程结束?

在线程对象内设置CEvent成员变量,当事件被置位时控制函数跳出循环返回,线程结束。

3. 多个线程如何一次关闭?

串行方法:对每线程依次完成通知结束、等待线程结束、删除对象的工作,效率低;
并行方法:先通知所有线程,等待全部线程退出后,再统一完成删除工作,效率高。

2008年11月9日星期日

创建工作者线程的方法--多线程编程

直到现在我在工作中接触到的多线程任务都是“无需用户输入”的,也就是说很少涉及到窗口消息等,一般就是计算、和I/O接口的通讯,这些都属于MFC定义的工作者线程(worker thread)范畴。工作者线程的开启(包括退出)是多线程编程入门级技术。虽然启动一个工作者线程非常简单,有时候一个CreateThread函数即可,但稍微深究便发现要游刃有余的运用它必须要有较全面的理解。

1. 关于::CreateThreadCWinThread::CreateThread

初学者(如我)在看MSDN以及既成代码的时候,遇到CreateThread就只知道是创建线程,而不知其名虽同,姓却不同,完全不是一家人。::CreateThread是Windows API函数,程序员直接调用该函数生成的只是类似MFC概念中的工作者线程;CWinThread::CreateThread是MFC类库函数,可以生成真正MFC概念中的工作者线程或用户接口线程。程序员直接调用::CreateThread的位置通常不在CWinThread或其派生类中,可去掉二元作用域操作符“::”,直接写作“CreateThread”,这样就更容易让初学者迷惑而不区分这两种完全不同的函数。

区分这两种函数的关键是理解CWinThread类的运用,为什么要把线程抽象成一个类?

回想线程的概念,它是进程的执行路径,进程的所有代码和数据空间被进程内所有的线程共享,如全局变量。于是我们知道,线程是一种有方法(函数代码)有属性(数据空间)的事物,可被抽象;另外,有些数据是不想在线程以外的位置被修改的,虽然可以用代码对全局变量进行保持和维护,但增加了工作量且易出错。于是,MFC便将线程抽象出来,定义了CWinThread类。CWinThread对象不但管理线程的局部变量,还提供了对MFC“消息泵”的支持。使用CWinThread及其派生类,使得代码和MFC都具有完全线程安全的特性,该特性要求任何使用到MFC的线程必须是CWinThread或其派生类

2. 创建非面向对象的简单工作者线程

非面向对象的简单工作者线程,是指控制函数不具有面向对象性,可被所有线程调用。一般说来有三种方法::CreateThreadAfxBeginThreadCWinThread::CreateThread。说到这里可能又会引起一些迷糊,不是说MFC引入了面向对象特性么,为什么用MFC函数(后两个)创建的却是非面向对象的工作者线程呢?这涉及到回调(callback)函数的概念。线程函数由程序员编写、由操作系统根据函数指针进行调用,这种类型的函数有别于直接用函数名进行调用的函数,被称作回调函数。回调函数不能有隐含的this指针,故只能是全局函数或类的静态成员函数。因此简单工作者线程只能是非面向对象或具有非常有限的面向对象特性。

创建简单工作者线程需要定义一个型如:

UINT MyControllingFunction ( LPVOID pParam );
的控制函数。参数为一个32-bit无符号整形,可以是一个具体数据、一个指针,也可以为空;返回“0”代表正常,其他值代表错误类型;&MyControllingFunction作为回调函数地址。

MSDN文档中对用::CreateThread、AfxBeginThread创建线程的方法有很详细的介绍了,只要把回调函数地址和控制函数的参数作为实参填入适当位置便可。文档中只说了AfxBeginThread最终调用CWinThread::CreateThread启动线程,而CWinThread::CreateThread的参数表中也没有可填回调函数地址的地方,看似无法用其创建工作者线程,其实恰恰相反,三种方法中最为灵活的便是CWinThread::CreateThread能创建简单工作者线程,只要借助两个文档中没有提到的CWinThread类公共成员变量:AFX_THREADPROC m_pfnThreadProc(回调函数地址)、LPVOID m_pThreadParams(控制函数参数),启动线程前显式将这两个参数赋上适当的值。

3. 创建具有面向对象特性的工作者线程

虽然有回调函数的要求,我们还是能够创建具有面向对象特性的工作者线程。具有面向对象特性的工作者线程有如下特点:1)没有消息循环;2)定义一个函数,可被不同线程调用,且仅影响线程内的局部变量。有两种方法

方法一:定义具有面向对象特性的控制函数,型如:

UINT MyThread::MyControllingFunction();
再定义类的静态成员函数作为回调函数,型如:
static UINT MyThread::MyCallbackFunction ( LPVOID pParam );
将this作为参数传递给该静态成员函数,并在其中调用类的成员函数:this-> MyControllingFunction()。该方法的具体实现可用AfxBeginThreadCWinThread::CreateThread。它有个局限,实际控制函数的参数只能为空。

方法二:不定义回调函数(控制函数地址为NULL),直接重载CWinThread::InitInstance(经常)或CWinThread::Run(几乎不)作为控制函数。观察MFC与操作系统接口的线程入口函数_AfxThreadEntry便知其原因:

UINT APIENTRY _AfxThreadEntry(void* pParam)
{

// first -- check for simple worker thread
DWORD nResult = 0;
if (pThread->m_pfnThreadProc != NULL)
//该方法中将此参数设为NULL
{
nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);
ASSERT_VALID(pThread);
}
// else -- check for thread with message loop
else if (!pThread->InitInstance())
//重载InitInstantce作为控制函数
//若重载InitInstance需将其返回值设为FALSE
{
ASSERT_VALID(pThread);
nResult = pThread->ExitInstance();
}
else
{
// will stop after PostQuitMessage called
ASSERT_VALID(pThread);
nResult = pThread->Run(); //或者重载Run作为控制函数
}

}
CWinThread::Run()函数支持“消息泵”,是用户接口线程的执行地址,若将其重载为工作者线程的控制函数将不再具有这个特性。该方法的具体实现亦可用AfxBeginThreadCWinThread::CreateThread,还可通过变量m_pThreadParams设置控制函数的参数。另外,由该方法可知MFC的工作者线程控制函数不一定是回调函数,MFC已经为操作系统提供了一个回调函数_AfxThreadEntry

本文介绍了5种工作者线程的创建方法,编程时可根据线程中是否用到MFC、是否需要面向对象特性等进行选择。

三种不同的线程概念--多线程编程

三种不同的线程: Windows线程、MFC工作者线程、MFC用户接口线程(User-Interface thread)

要理解这三个不同的概念,首先要理清Windows API和MFC的关系。Windows API是Windows操作系统为编程者提供的接口函数,为所有基于C/C++语言的应用程序利用系统资源和被操作系统调度提供接口。Windows API是操作系统级的通用接口,直接用其编程将会有大量重复性工作并且极易出错、不易调试。MFC为了使常用的任务,如创建窗口等变得简单、不易出错,提供了一组函数给用户,它实际是用额外的代码对Windows API函数进行了包装,引入了面向对象的概念。于是,一般说MFC是编译器级别的接口,它只适用于某公司(如Microsoft)开发的编译器。

Windows操作系统支持多线程操作,特别是基于32位寻址的WinNT内核的操作系统,如Windows2000、WindowsXP支持抢占式多任务操作,提供的应用程序接口被称为:Win32 API。如无特别说明,下面的API皆指Win32 API。

Win32 API提供了有关多线程操作的接口函数,操作系统只要知道线程函数的起始地址便可开始执行线程,没有所谓工作者线程和用户接口线程的区别,即使用MFC开启的不同线程到了操作系统级别就一视同仁了。

·MFC为什么要提出两种不同的多线程概念呢?
为了适应它的面向对象特性。前面说过MFC引入了面向对象的思想,它得以实现的关键技术之一就是消息映射(Message Map)和命令传递(Command Routing)。我们知道,推动Windows运转的是一系列的消息,“消息泵(Message Pump)”使得这些消息也具有了面向对象的特质,也就是说,归属某一类的消息只会进入该类并引发该类成员函数的调用。当用户有了输入动作的时候将会产生一个消息,这个消息进入消息循环、并“泵”到相应的类中。如果需要开启的线程有用户输入动作,它必须要有自己的“消息泵”,程序的进行根据消息推动,这就是用户接口线程(User-Interface thread);若开启的线程不需要响应用户动作,则可直接将它的任务写成简单控制函数,线程函数直接调用该控制函数,这就是工作者线程(worker thread)。

·MFC如何利用Windows API实现两种不同的线程呢?
上文说过MFC是对Windows API函数的包装,利用MFC写的程序归根到底还是要转换到Windows API函数,MFC如何将Windows线程变成不同的两种呢?
跟踪MSDN示例程序“mtdgi”的运行进入thrdcore.cpp文件,原来MFC提供给操作系统的多线程函数入口地址统一为&_AfxThreadEntry,函数_AfxThreadEntry中再根据程序员可间接修改的参数而进行分支。

VC++学习 Step by Step(序)

3年前就开始在课题中使用VC6.0编程
C和C++在大学就接触了
但,到现在仍不敢说自己能够用VC6.0进行VC++系统设计
因为做的实际工作大多是继承师兄已有的项目
修改bug、完善系统经常是就事论事
知识体系零散破碎
硕士的师兄喜欢用基于DLG编程
博士的师兄做的基于MDI的项目
也许很多都是基本和已经落伍的技术
还是必须在现有平台下好好学好一门语言
这个不关乎老板的要求
关乎自己的积累和认识的提高
要乐在其中了就不那么容易被外界的评论所动摇
就不容易被浩浩荡荡的编程大队伍震慑住自己的决心和动力

2008年10月28日星期二

为什么Win32 API函数可在MFC工程中直接调用?

Win32 API函数在<winbase.h>中申明
<winbase.h> 包含于 <windows.h> 包含于 <afxv_w32.h>
包含于 <afxver_.h> 包含于 <afx.h> 包含于 <afxwin.h>
包含于 "stdafx.h" 包含于 由向导生成的MFC工程

MFC工程基于C++建立
若需调用的Win32 API函数与当前类的某成员函数重名,需加"::"二元作用域运算符
若无重名,可直接调用