Windows环境下使用MFC进行多线程编程(C++)

转载请标明出处: http://blog.csdn.net/zhangxingping

Windows环境下使用MFC进行多线程编程(C++)

进程和线程 (Process andThread)

进程指的是计算机程序的一次执行,或者说是程序执行的一个实例。进程中包含有程序代码及其当前的活动。线程指的是进程中的一个执行路径。一个进程有可能是由多个线程组成,这些线程并发地执行程序指令。

在传统的操作系统中, CPU调度和分派的基本单位是进程。而在现代操作系统中(引入了线程的操作系统中),则把线程作为CPU调度和分派的基本单位,而把进程作为资源分配的基本单位,从而使得传统进程的两个属性分开了,这样可以显著提供系统的并发性。

在引入线程的操作系统中,不仅进程之间是可以并发执行的,而且同一个进程间的线程也是可以并发执行的,因此更好地提供了操作系统的并发性,从而更有效地利用系统资源和提高系统的效率。

MFC中的线程相关知识

MFC中有两种类型的线程:用户接口线程和工作线程。用户接口线程通常是用来处理用户输入,相应用户事件和消息的。工作线程通常是用来完成实际任务的,比如不需要用户输入的复杂计算等。Win32 API中是没有这两中线程的区分的,其中只需要线程开始执行的地址就可以了。MFC则为用户接口线程提供了消息机制,用来处理相关事件。CWinApp就是一个担心的用户接口线程对象,因为它是从CWinThread继承而来,并且能处理用户生成的事件和消息。

创建线程

Windows环境下,创建线程可以通过函数AfxBeginThread()来完成。该函数有两种重载的形式,分别用来创建工作线程和UI线程:

CWinThread* AfxBeginThread(

  AFX_THREADPROC pfnThreadProc,

  LPVOID pParam,

  int nPriority = THREAD_PRIORITY_NORMAL,

  UINT nStackSize = 0,

  DWORD dwCreateFlags = 0,

  LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

CWinThread* AfxBeginThread(

  CRuntimeClass* pThreadClass,

  int nPriority = THREAD_PRIORITY_NORMAL,

  UINT nStackSize = 0,

  DWORD dwCreateFlags = 0,

  LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

在这里,先对上述两种形式中的共有的参数的含义进行描述:

参数

含义

nPriority

线程的优先级别。缺省值为NORMAL。更多信息参见SetThreadPriority

nStackSize

线程运行时期望的栈的大小。缺省值和创建线程的大小一样。

dwCreateFlags

如果想在线程创建后挂起该线程,可以传入CREATE_SUSPENDED。缺省值为0,表示正常启动该线程。

lpSecurityAttrs

线程的属性。缺省属性和父线程一样.更多信息请参见SECURITY_ATTRIBUTES 的描述。

创建UI线程

创建UI线程时,使用上面的第二中重载形式。其中只有一个必选参数:pThreadClass。该参数为一个从CWinThread派生而来的类的运行时类型。这就是说,我们需要从CWinThread类派生出自己的线程类,并实现该类。注意:声明和实现这个类的时候必须使用宏DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE。该类必须对继承而来的某些方法进行重写(overriding)。下面的表格给出了继承方法的描述:

方法

目的

ExitInstance

当线程退出时,进行清理工作. 通常都需要重写.

InitInstance

完成线程的初始化工作. 必须被重写.

OnIdle

进行一些和线程相关的空闲时的处理。通常不对其进行重写.

PreTranslateMessage

在消息被派发给TranslateMessage DispatchMessage之前进行过滤。通常不对其进行重写。

ProcessWndProcException

拦截线程的消息和命令处理器抛出的未处理的异常。通常不对其进行重写。

Run

线程的控制函数。其中含有消息泵。几乎从不需要对其进行重写。

该重载形式中的其它参数都是可选的。

从上面的描述可以看出,创建并启动UI线程一共需要两个步骤:

1.        声明并实现自定定义的派生类,该类必须为CWinThread的派生类。

2.        调用AfxBeginThead启动UI线程。

创建工作线程

上面提到的AfxBeginThread函数的第一种重载形式就是用来启动一个工作线程的。这种重载形式中除了前两个参数之外,其它参数都在前文中进行了描述。下面重点对该形式的前两个参数进行描述。

参数

含义

pfnThreadProc

线程控制函数的入口地址

pParam

传递给线程控制函数的参数

从上面的描述中可以看出,在创建工作线程之前,我们必须先定义工作线程的入口函数。工作线程入口函数的原型如下:

UINT MyControllingFunction( LPVOID pParam);

入参pParam就是调用AfxBeginThread是传入的第二个参数,其类型为LPVOID,可见在入口函数中是可以按照任何所需的形式来对齐进行解析的。返回的UINT值表示了线程结束的具体原因。

从上面的描述可见,创建并启动一个工作线程一共需要两个步骤:

1.        声明并实现线程的入口函数。

2.        调用AfxBeginThread函数来启动线程。

线程间的通讯

在多线程的环境下,线程间的通讯往往是必要的。比如主线程创建了子线程来完成某个任务,那么主线程至少要告诉子线程具体的任务是什么;并且,子线程在完成了该任务后需要通知主线程任务完成及其结果是什么。这就需要用到线程间的通讯。

前面说过,在现代操作系统中,进程是进行资源分配的基本单位。因此,分配给一个进程的资源是该进程中所有线程都可以访问的。所以,使用全局变量便是一个最简单的线程间通讯的方式了。由于是全局变量,同一个进程中的所有线程都是可以访问这些变量的,这就包括对齐进行读写操作。这样一来,可能出现在多个线程中对这些变量进行修改的情况,因此这些变量最好声明成为是volatile的。

第二种线程间的通讯方式就是发送消息了。前面也提到过MFC中的UI线程支持消息机制,因此,采用这种方式进行通讯要求线程都是从CWinThread派生而来的。CWinThread类中提供了一个成员函数PostThreadMessage,调用该函数可以向另外的线程发送消息。

关于线程的同步

多线程技术虽然能够提高程序执行的并发性和系统资源的利用率,但同时也引入了新的问题。比如,系统中的某些独占性资源,如打印机等。由于线程可以执行进程中的任何代码块,那么很有可能两个线程都执行了打印相关的代码块,这样打印的结果很有可能就不是所期望的。再比如,进程中有一个核心数据结构链表,很有可能在有一时刻,某个线程需要对其执行插入节点操作,而另外一个线程需要对齐执行删除或者遍历操作;由于线程是CPU进行调度和分配的基本单位,此时如果不加以控制,就很有可能得到错误的结果。诸如上面的两个例子中,这些线程间的调度存在一定的先后顺序,以便保证对数据访问的完整性和线程对资源访问顺序的合理性。因此,多线程程序中,线程的同步是很有必要的。

MFC中提供了如下可用于线程间同步的类:

CSyncObject

CSemaphore,

 CMutex,

 CCriticalSection,

CEvent

CMultiLock 

CSingleLock

这些类可以分成两种类型:同步类和同步访问类。同步类指的是在访问资源时必须加以控制以确保对资源访问的完整性的类,这就包括了前5个类;而同步访问类指的是用来获取对这些受控资源进行访问的类,包括了最后两个类。

值得注意的是上述类中的CSyncObject,类是一个纯虚类,因此在实际编程时很少直接使用。

关于这些类的具体使用方法,后续会有专门的博文进行描述,敬请关注。

终止线程

线程的终止有两种情况:线程的控制函数执行完毕以及线程被要求停止执行,或者称为正常结束和提前结束。

对应一个工作线程来说,正常结束可以是入口函数执行到了return语句或者是执行到了AfxEndThread()函数。入口函数的返回值或者是AfxEndThread函数中的第一个参数表示了线程终止的具体原因。通常用0来表示正常结束,当然这个取决于自己程序中的定义。对于一个UI线程来说,正常结束也很简单:在UI线程中调用API PostQuitMessage,并传入一个整型数作为线程退出的具体原因即可。和工作线程一样,通常用0来表示正常结束。

如果需要提前结束线程,只需要在线程中调用AfxEndThread,并传入表示退出原因的退出码即可。调用该函数将停止线程的执行,释放线程的栈,并从内存中删除线程对象。

需要注意的一点是必须在需要终止的线程中调用AfxEndThread。如果需要在别的线程中终止该线程,则必须在涉及的两个线程中建立通信机制,比如前文中提到的使用全局变量或者发送消息的方法。

如果需要获知某个线程的退出码,不管是工作线程还是UI线程,都可以调用GetExitCodeThread,该函数需要一个指向线程的句柄和一个指向用于存储退出码的DWORD类型的指针。

需要注意的是,获取CWinThread对象的退出码时,可能需要一些额外的操作。缺省情况下,当一个CWinThread线程结束后,其中的线程对象就会被删除,这意味着访问其m_hThread数据成员将没有意义,因为CWinThread对象已经不存在了。为了避免这种情况的发生有以下两种做法:

1.        设置其m_bAutioDelete成员为FALSE。这将使得CWinThread对象在线程结束后依然存在。这样就可以访问其m_hThread数据成员了。采用这种方法是,CWinThread对象的销毁有程序员自己根据需要进行,因为此时MFC的framework将不会自动销毁给对象。这是首选的做法。

2.        将线程的句柄单独存储起来。在线程被创建后,使用::DuplicateHandle来复制其数据成员m_hThread到另外的变量中。此时,即使CWinThread对象已经被销毁了,我们仍然是可以调用GetExitCodeThread来获取其退出码的。此时需要确保在复制句柄的时候,线程没有终止。一个安全的做法是给AfxBeingThread传入CREATE_SUSPENDED,保存句柄,然后调用ResumeThread来继续线程的执行。

多线程编程相关友情提示

出于性能方面的考虑,MFC对象都不是线程安全的,仅仅只是类级别安全的。这就意味着我们可以在两个线程中处理两个不同额CString对象,但是不能在两个线程中处理同一个CString对象。如果确实需要在多个线程中对同一个对象进行处理,那就必须要用到Win32中的同步机制及其相关的类。


发布了22 篇原创文章 · 获赞 6 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/zhangxingping/article/details/8492629
今日推荐