VC_中基于MFC的多线程应用程序设计

VC_中基于MFC的多线程应用程序设计

2023年7月18日发(作者:)

第19卷第2期三明高等专科学校学报2002年6月            Vol119No12JOURNALOFSANMINGCOLLEGEJun12002VC++中基于MFC的多线程应用程序设计陈少强(三明高等专科学校计算机科学系,福建三明 365004)[摘要]介绍了Windows的多线程技术与编程思想,着重讲述了利用Visual C++中MFC进行多线程应用编程的过程及实现方法。比较全面地阐述了如何基于MFC实现多线程编程。[关键词]进程;多线程;MFC;同步对象[中图分类号]TP31111  [文献标识码]A  [文章编号]1671-1343(2002)02-0049-07多线程是现代操作系统(如Windows95、WindowsNT)中出现的概念。随着面向对象编程思想和一些面向对象高级语言的广泛采用,编写多任务的应用程序已经是一件很普通的工作。用进程和线程的观点来研究软件的设计是当今普遍采用的方法。进程和线程概念的出现,对提高软件的并行性有着重要的意义。现在的应用软件越来越注重多线程多任务的处理。因此掌握多线程多任务设计方法对每个程序员都是必需掌握的。1 多任务、多进程和多线程当前流行的Windows操作系统,其重要特征之一是引入了多进程和多线程机制,支持多任务调度和处理,由此提供了多任务空间。所谓多任务通常包括两大类:多进程和多线程。进程是指在系统中正在运行的一个应用程序,每个进程是由私有的虚拟地址空间、代码、数据和其它系统资源组成;线程是进程之内独立执行的一个单元,是系统分配处理器时间资源的基本单元。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,每个线程完成一个特定的任务,多个线程并行地运行在同一进程中,最终完成多个任务。这就是所谓基于多线程的多任务。一个Win32应用程序可以在Windows平台上运行多个实例,每个应用程序实例都是一个独立的进程,而一个进程可以由不只一个线程来实现。这样的应用程序就是一个多线程应用程序。还应注意,同一个进程中的所有线程共享进程的虚拟地址空间和全局变量,但同时每个线程可以拥有自己的变量。多线程应用程序比单线程应用程序要考虑更多的因素,但它有着明显的好处。首先,对于操作系统而言,其调度单元是线程。每个线程被分配一个CPU时间片,一旦被激活,它正常运行直到时间片耗尽并被挂起,此时,操作系统选择另一线程进行运行。由于时间片很小,通过时间片轮转,看起来就像多个线程同时在工作,这样挖掘了宝贵的CPU时间资源,提高了CPU的使用效率。第二,在多线程应用程序中,一个程序有几个任务要同时运行,经常可以组织为多个并行的、执行独立的线程,可以用线程给不同的程序任务赋予不同的优先权,使那些重要的程序任务能得到更多的CPU时间。第三,如果应用程序运行在多处理器系统中,可以将各任务分布到几个线程中,让它们在独立的处理器中同时运行。[收稿日期]2002203213[作者简介]陈少强(1972-),男,福建三明人,三明高等专科学校计算机科学系讲师。49开发多线程应用程序可利用32位Windows环境提供的Win32API接口函数,也可利用VC++中提供的MFC类库。多线程编程在这两种方式下原理是一样的,用户可以根据需要选择相应的工具[1]。本文重点讲述用VC++610提供的MFC类库实现多线程调度与处理的方法,以及由线程多任务所引发的同步多任务特征。2 基于MFC的多线程在VisualC++610附带的MFC类库中,提供了多线程编程的支持,基本原理与基于Win32API函数的设计一致,但由于MFC对同步对象作了封装,因此对用户编程实现来说更加方便,避免了对象句柄管理上的繁琐工作。更重要的是,在多个窗口线程情况下,MFC中直接提供了用户接口线程的设计。在MFC中,线程分为两种:用户接口线程(UserInterfaceThread)和工作者线程(WorkerThread)。前者常用来独立地处理用户输入和响应用户事件。后者常用于任务处理不要求用户输入的后台任务,执行这些后台任务并不会耽搁用户对应用程序的使用,即用户操作无需等待后台任务的完成。用户接口线程常用于接收用户的输入,处理相应的事件和消息。在用户接口线程中,包含一个消息处理循环,其中CWinApp就是一个曲型的例子,它从CWinThread派生出来,负责处理用户输入产生的事件和消息。下面介绍这两类线程的设计与实现。211 工作者线程要创建一个线程,需要调用函数AfxBeginThread。该函数因参数重载不同而具有两种版本,分别对应工作者线程和用户接口线程。用于创建工作者线程的AfxBeginThread函数有六个参数。其中四个参数表示了当前线程的优先级、堆栈大小、创建标志和安全属性,它们都有默认值,可不用改变。但编程时一定要提供其中的两个参数,这两个参数是控制函数和传递给控制函数的参数。工作者线程编程较为简单,只需编写线程控制函数和启动线程即可。(1)实现控制函数。控制函数定义该线程。当进入该函数,线程启动;退出时,线程终止。该控制函数声明如下:UINTMYControllingFunction(LPVOIDpParam);控制函数定义了线程要执行的代码。因此,对于工作者线程来说,启动一个线程,首先要编写一个希望与应用程序的其余部分并行运行的函数如MyWorkThread()。(2)启动线程。在程序中调用线程的函数,由函数AfxBeginThread创建并初始化一个CWinThread类的对象,启动并返回该线程的地址,则线程进入运行状态。其形式如下:AfxBeginThread(MyWorkThread,param,priority);其中MyWorkThread是线程要运行的函数的名字,也既是上面所说的控制函数的名字,param是准备传送给线程函数MyWorkthread的任意32位值,priority则是定义该线程的优先级别。这样,就启动了线程来执行上面的MyWorkThread()函数。(3)中止线程。对于工作者线程来说,正常地中断线程很简单,控制函数运行结束,线程就中止了,同时返回一个说明终止原因的值,0表示执行成功。也可以利用AfxEndThread()函数。212 用户接口线程我们知道,基于MFC的应用程序有一个应用对象,它是CWinApp派生类的对象,该对象代表了应用进程的主线程。当线程执行完(通常是接收到WM QUIT消息)并退出线程时,由于进程中没有其它线程的存在,故进程也自动结束。类CWinApp从CWinThread派生出50来,CWinThread是用户接口线程的基本类。我们在编写用户接口线程时,需要从CWinThread派生我们自己的线程类,ClassWizard可以帮助我们完成这个工作。与工作者线程相比,用户接口线程的实现则有显著的不同,下面从编程角度讨论其实现方法。(1)MFC派生用户接口线程类。用ClassWizard派生一个新的类,设置基类为CWinThread。需要注意,类的DECLARE DYNCREATE和IMPLEMENT DYNCREATE宏是必需的,因为创建线程时需要动态创建类的对象。根据需要可将初始化和结束代码分别放到类的InitInstance和ExitInstance函数中。InitInstance()函数进行线程实例的初始化工作,ExitInstance()完成线程结束时的清理工作。如果需要创建窗口,可在InitInstance函数中完成。(2)创建线程并启动线程。可以用两种方法来创建并启动用户接口线程。①MFC提供了两个版本的AfxBeginThread函数,其中一个用于创建用户接口线程,函数原型如下:CwinThread3AfxBeginThread(CRuntimeClass3pThreadClass,intnPriority,UINTnStackSize,DWORDdwCreateFlags,LPSECURITY ATTRIBUTESlpSecurityAttrs);其中,参数pThreadClass指定线程的运行类,函数返回线程对象。在程序中调用该函数就可以启动一个线程,其使用方式与工作者线程相同。②我们也可以不用AfxBeginThread创建线程,而是分两步完成:首先调用线程类的构造函数创建一个线程对象,接着调用CWinThread::CreateThread函数来启动该线程。注意:在这种情况下,在线程类中需要有公有的构造函数以创建其相应的C++对象。与前一方法不同的是,在线程终止后,该线程对象依然存在,可以再次调用CreateThread函数来启动线程。③线程的悬挂、恢复。CWinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数。在创建线程时,可以指定线程先挂起,若用AfxBeginThread函数创建,将参数dwCreate2Flags设置为设置为CREATE SUSPENDED,若用CreateThread函数创建,将CreatFlagsCREATE SUPEND,或在执行时用SuspendThread()来悬挂线程;然后做一些初始化工作,如对变量赋值等。最后,再调用线程类的ResumeThread函数恢复线程运行。需要注意的是,如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的Re2sumeThread()来恢复线程。④线程的终止。结束一个用户接口线程的执行有多种方法。线程可以在自身内部调用ExitThread()或AfxEndThread()来终止自身的运行并释放线程所占用的资源;可以在线程外部向其发送消息,引发线程调用上述函数终止线程;或在线程的外部调用TerminateThread()来强行终止一个线程的运行,然后调用CloseHandle()函数释放线程所占用的堆栈,在实际程序设计中对该函数的使用一定要谨慎,因为一旦该命令发出,将立即终止该线程,并不释放线程所占用的资源,这样可能会引起系统不稳定。需要注意的是,终止一个进程将会结束它所拥有的所有线程。如果所终止的线程是进程内的最后一个线程,则在该线程终止之后进程也相应终止。3 线程的优先级在Windows95和WindowsNT操作系统中,任务是有优先级的,共有32级,从0到31,系统按照不同的优先级调度线程的运行。其中,0~15级是普通优先级,线程的优先级可以动态变化。高优先级线程优先运行,只有高优先级线程不运行时,才调度低优先级线程运行,优先级相同的线程按照时间片轮流运行。16~30级是实时优先级,实时优先级与普通优先级的最51大区别在于,相同优先级进程的运行不按照时间片轮转,而是先运行的线程就先控制CPU,如果它不主动放弃控制,同级或低优先级的线程就无法运行。一个线程的优先级首先属于一个类,称为进程类基本优先级,然后是其在该类中的相对位置,称为线程相对优先级。线程优先级的计算可用如下式子表示:线程优先级=进程类基本优先级+线程相对优先级可以设置用户界面线程的优先级。调用创建线程函数启动线程时,对参数的赋值决定了线程运行后的优先级。还可以在线程启动后,调用线程类的成员函数GetThreadPriority()和SetThreadPriority()来获得和设置当前线程的优先级。对于进程类基本优先级的设置,CwinThread类没有提供相应的函数,但是可以通过Win32SDK函数GetPriorityClass()和SetPriorityClass()来实现。4 线程同步在有若干个线程并行运行的环境里,线程之间经常要同时访问一些共享资源,不同线程之间的同步是至关重要的。例如,一个线程正在更新一个结构的内容的同时,另一个线程正试图读取该结构。结果,我们将无法得知所读取的数据是什么状态。再如,两个线程协同完成一个任务,线程B要运行,必须等待线程A运行到某一时刻,发给线程B一个信号后,处于等待状态下的B才可继续往下运行。线程的这种同步问题如果不解决,就会出现资源竞争而引起几[2]个线程甚至整个系统的死锁,直接涉及到整个系统的稳定和安全。因此,不管是工作者线程还是用户接口线程,在存取共享资源时,都需要保护共享资源,以免引起冲突,造成错误。目前流行的Windows操作系统,提供了几种同步对象,包括临界区、互斥量、信号量和事件等。这些同步对象能够让各个线程协调工作,实现同步,使程序运行起来更安全。下面简要介绍这几个同步对象。411 临界区临界区是保证在某一个时间只有一个线程可以访问数据的方法。使用它的过程中,需要给各个线程提供一个共享的临界区对象,无论哪个线程占有临界区对象,都可以访问受到保护的数据,这时候其它的线程需要等待,直到该线程释放临界区对象为止,临界区被释放后,另外的线程可以强占这个临界区,以便访问共享的数据。注意,临界区只可由单个进程的线程使用。临界区对应着一个CcriticalSection对象,当线程需要访问由该对象所控制的资源时,创建一个CmultiLock或CsingleLock类型的加锁变量,并且调用加锁对象的Lock()成员函数;当对所控制的资源操作完成之后,调用加锁变量的Unlock()成员函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。412 互斥量互斥量与临界区很相似,但是使用时相对复杂一些,它不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。互斥量允许在任意时刻有且仅有一个线程或进程访问某资源。互斥量必须处于两种状态之一:有信号的和无信号的。当互斥量处于有信号状态时,第一个等待该互斥量的线程将被唤醒,并重新将该互斥量置为无信号状态,以免多个线程被同时唤醒。一旦拥有该互斥量的线程执行完成,它必须释放该互斥量,这时,该互斥量重新处于有信号状态,第二个等待该互斥量的线程将被唤醒,拥有该互斥量,获得执行的机会。在多个线程同时等待同一个互斥量对象的情况下,当互斥量处于有信号状态时,总是优先级最高的那个线程先被唤醒,优先级低的线程被迫继续等待。52413 信号量信号量对象允许多个线程访问某个共享资源,采用一计数器来实现信号量。每当有一个或多个资源变成可用的,可用的资源计数就减1。信号量能自动地进行测试和设置操作。当从一个信号量请求资源时,操作系统负责检查该资源是否可用,如果可用,就将其计数器减1,并允许线程访问该资源。当计数为0时,任何试图从该信号量请求资源的线程都被迫等待,等待计数重新变成大于0,这时线程才被允许访问信号量对象控制的资源。信号量与互斥量或临界区是有区别的,通过信号量,可以同时有若干线程得到由一个信号量对象控制的资源的访问权,而互斥量或临界区在任意时刻只能有一个线程访问所控制资源。414 事件事件同步对象与前面的同步对象有很大的不同。互斥量和信号量通常用来控制对数据或资源的访问,而事件是用来发信号以通知其他的线程某一操作已经开始或完成。有两种不同类型的事件对象:人工重置事件和自动重置事件。人工重置事件用于一次向多个线程同时发信号以表示某一操作已经开始或完成,而自动重置事件用于向单个线程表明某一操作已开始或完成。在VisualC++中,可以利用Win32函数进行多线程同步控制,但MFC为我们提供了几个同步对象C++类,如果我们使用MFC类库,就可利用已经封装成C++结构的同步对象,使我们的编程更加简捷。MFC对上述的四种同步对象均进行了封装,对应的四个MFC同步类分别是:CCriticalSection、CMutex、CSemaphore和CEvent,这四个类都从CSyncObject类直接派生出来的。CSyncObject是一个抽象类,用来实现Win32的同步对象最基本的功能。MFC还提供了另外两个同步访问类:CMultiLock和CsingleLock,同步访问类用来获得对这些控制资源的访问。CMultiLock和CSingleLock的区别仅在于是需要控制访问多个资源对象还是单个资源对象。415 同步类的使用方法通常,我们在C++对象的成员函数中使用共享资源,或者把共享资源封装在C++类的内部。我们可将线程同步操作封装在对象类的实现函数当中,这样在应用中的线程使用C++对象时,就可以像一般对象一样使用它,简化了使用部分代码的编写,这正是面向对象编程的思想[3]。这样编写的类被称作“线程安全类”。要设计一个线程安全类,首先根据具体情况在类中加入同步类作为数据成员。然后,在使用共享资源的函数中,将同步类与同步访问类的一个锁对象联系起来。即:在访问控制资源的成员函数中应该创建一个CSingleLock或CMulti2Lock的对象,然后调用该对象的Lock函数。当对象结束时,自动在析构函数中调用Unlock函数,当然也可以在任何希望的地方当访问结束之后调用Unlock函数释放资源。以上介绍的四个同步类的使用方法几乎是一样的。怎样选择同步对象,应遵循下面的简单规则:事件同步类CEvent通常用于在应用程序访问资源之前,必须等待某个事件发生的情况;信号同步类CSemaphore通常用于当一个应用程序中有多个线程要访问同一个资源的情况;互斥同步类CMutex和临界区同步类CCriticalSec2tion都是用于保证一个资源一次只能有一个线程访问,二者的不同之处在于前者允许有多个应用程序使用该资源,后者只允许在一个应用程序中的线程使用,但使用临界区的方式效率比较高。416 多线程实例下面的例子中同时运行两个线程,线程A把10个数赋予公共数组Array[],线程B从数53组Array[]中依次取出数存入数组Brray[]中。显然两个线程对数组Array[]的访问要求互斥,也即仅当10个数全部存入数组Array[]中线程B才能从数组Array[]中取数。以下是采用互斥量对象解决这两个线程同步问题的主要代码。#include″afxmt.h″intArray[10],Brray[10];CMutexSection;          //创建一个CMutex类的对象AfxBeginThread(WriteThread,hWnd);//启动线程AAfxBeginThread(ReadThread,hWnd); //启动线程BUINTWriteThread(LPVOIDparam)  //编写线程A函数 {CSingleLocksinglelock;        //创建一个CSingleLock类的加锁对象singlelock(&Section);();         //调用加锁对象Lock成员函数for(intx=0;x<10;x++)      //对由该互斥量对象所控制的资源array[]访问array[x]=x;();         //调用加锁对象Unlock函数,释放资源 }UINTReadThread(LPVOIDparam)  //编写线程B函数 {CSingleLock snglelock;singlelock(&Section);();for(intx=0;x<10;x++)Brray[x]=array[x];(); }5 注意事项多线程应用程序设计是一种较为新颖的编程技术,在程序设计思路上不同于传统的模块结构化方法,比一般的面向对象的思路也较复杂,尤其是对于多处理器平台的处理更为复杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或不能同步处理的现象。因此要求编程人员必须从思想上加倍注意自己的多线程程序。基于MFC编程,如前所述使用线程安全类方法,比不考虑线程安全要复杂,尤其体现在程序调试过程中。目前,大多数的计算机都是单处理器(CPU)的,在这种机器上运行多线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为了抢夺对CPU的控制权,则在线程切换时会消耗很多CPU资源,但对于大部分时间被阻塞的线程(例如等待文件I/O操作),则可用一个单独的线程来完成。这样,就可将CPU时间让出来,使程序获得更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视具体情况加以处理,使应用程序获得最佳的性能。6 结束语对复杂的应用程序来说,线程的应用给应用程序提供了高效、快速、安全的数据处理能力。54本文对VC++中基于MFC的多线程应用程序设计作了比较全面的介绍。[参考文献][1]C++技术内幕[M].北京:清华大学出版社,1999.[2]刘勇.如何利用MFC实现线程间的同步[J].计算机应用,2001,S1.[3]于华.多线程应用程序中的同步控制技术及应用[J].计算机系统应用,2001,07.(责任编辑:赖文忠)VC++MultithreadApplicationProgrammingBasedonMFCCHENShao2qiang(TheComputerScienceDepartment,SanmingCollege,Sanming365004,China)Abstract:IntroducesthemultithreadtechnologyandprogrammingideaofWindows,andmainlydescribestheprocedureandimplementmethodofprogrammingmultithreadapplicationbyuseofMFCofVisualC++.Thepaperdiscds:process;multithread;MFC;synchronizationobject55

发布者:admin,转转请注明出处:http://www.yc00.com/news/1689610546a270381.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信