《使用+C+++和+MFC+进行多线程编程.pdf》由会员分享,可在线阅读,更多相关《使用+C+++和+MFC+进行多线程编程.pdf(26页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、使用 C+和 MFC 进行多线程编程 Visual Studio 2005 其他版本 Microsoft 基础类(MFC)库提供多线程应用程序支持。本主题描述进程、线程和 MFC 多线程编程方法。进程是应用程序的执行实例。例如,双击“记事本”图标时,将启动运行“记事本”的进程。线程是进程内的执行路径。启动“记事本”时,操作系统创建进程并开始执行该进程的主线程。此线程终止时,进程也终止。启动代码以函数地址的形式将此主线程提供给操作系统。通常是所提供的 main 函数或 WinMain 函数的地址。如果愿意,可以在应用程序中创建其他线程。如果在处理后台任务或维护任务时不希望用户等待这些任务完成,则
2、可能需要创建其他线程。MFC 应用程序中的所有线程都由 CWinThread 对象表示。大多数情况下,甚至不必显式创建这些对象,而只需调用框架 Helper 函数 AfxBeginThread,该函数将为您创建 CWinThread 对象。MFC 区分两种类型的线程:用户界面线程和辅助线程。用户界面线程通常用于处理用户输入及响应用户生成的事件和消息。辅助线程通常用于完成不需要用户输入的任务(如重新计算)。Win32 API 不区分线程类型;它只需要了解线程的起始地址以开始执行线程。MFC 为用户界面中的事件提供消息泵,从而对用户界面线程进行专门处理。CWinApp 是用户界面线程对象的一个示例
3、,因为它从 CWinThread 派生并对用户生成的事件和消息进行处理。应特别注意以下情况:可能有不止一个线程需要访问同一对象。多线程编程:编程提示 介绍了一些可以避免在这些情况下可能发生的问题的技术。多线程编程:如何使用同步类说明如何使用可用的类从多个线程同步访问一个对象。编写和调试多线程编程本身是一项复杂棘手的任务,因为您必须确保一次只能有一个线程访问对象。多线程编程主题没有讲述多线程编程的基础知识,而只是说明了如何在多线程程序中使用 MFC。Visual C+中包含的多线程 MFC 示例阐释了几种多线程“添加功能”和 MFC 中未包含的 Win32 API,但只是一些入门知识。有关操作系
4、统如何处理进程和线程的更多信息,请参见 Platform SDK 中的进程和线程。多线程处理:创建用户界面线程 Visual Studio 2005 其他版本 用户界面线程通常用于处理用户输入和响应用户事件,这些行为独立于执行该应用程序其他部分的线程。已经创建并启动主应用程序线程(在 CWinApp 导出的类中提供)。本文描述创建其他用户界面线程所需的步骤。创建用户界面线程时,必须首先从 CWinThread 派生类。必须使用 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 宏声明并实现此类。此类必须重写某些函数,也可以重写其他函数。下表列出了这些函数及其用途。
5、创建用户界面线程时要重写的函数 函数 用途 ExitInstance 线程终止时执行清除。通常重写。InitInstance 执行线程实例初始化。必须重写。OnIdle 执行线程特定的闲置时间处理。通常不重写。PreTranslateMessage 将消息调度到 TranslateMessage 和 DispatchMessage 之前对其进行筛选。通常不重写。ProcessWndProcException 截获由线程的消息和命令处理程序引发的未处理异常。通常不重写。Run 控制线程的函数。包含消息泵。一般不重写。MFC 通过参数重载提供两个版本的 AfxBeginThread:一个用于用户界
6、面线程,另一个用于辅助线程。若要启动用户界面线程,请调用 AfxBeginThread,提供下列信息:从 CWinThread 派生的类的 RUNTIME_CLASS。(可选)所需的优先级级别。默认值为正常优先级。有关可用的优先级级别的更多信息,请参见 Platform SDK 中的 SetThreadPriority。(可选)所需的线程堆栈大小。默认值与创建线程的堆栈大小相同。(可选)CREATE_SUSPENDED,如果希望在挂起状态中创建线程。默认值为 0,即正常启动线程。(可选)所需的安全属性。默认值与父线程具有相同的访问权。有关此安全信息格式的更多信息,请参见 Platform SD
7、K 中的 SECURITY_ATTRIBUTES。AfxBeginThread 为您完成大部分工作。它创建类的新对象、使用您提供的信息初始化该对象并调用 CWinThread:CreateThread 开始执行线程。在整个过程中进行检查,确保假如创建过程的任何部分出现故障,所有对象都能被正确地解除分配。多线程处理:终止线程 Visual Studio 2005 其他版本 通常导致线程终止的两种情况是:控制函数退出或不允许线程完成运行。如果字处理器使用后台打印线程,若成功完成打印,则控制函数将正常终止。但是,如果用户要取消打印,后台打印线程则不得不提前终止。本主题介绍如何实现每一种情况,以及在终
8、止后如何获取线程的退出代码。正常线程终止 过早的线程终止 检索线程的退出代码 正常线程终止 对于辅助线程,正常线程终止很简单:退出控制函数并返回表示终止原因的值。可以使用 AfxEndThread 函数或 return 语句。一般情况下,0 表示成功完成,但这取决于您自己。对于用户界面线程,该过程也很简单:从用户界面线程内调用 Platform SDK 中的 PostQuitMessage。PostQuitMessage 采用的唯一参数是线程的退出代码。对于辅助线程,0 通常表示成功完成。过早的线程终止 过早终止线程几乎一样简单:从线程内调用 AfxEndThread。将所需的退出代码作为唯一
9、参数传递。这将停止执行线程、解除对线程堆栈的分配、分离附加到线程的所有 DLL 并从内存中删除线程对象。必须从要终止的线程内调用 AfxEndThread。如果要从其他线程终止线程,必须设置两个线程间的通信方法。检索线程的退出代码 若要获取辅助线程或用户界面线程的退出代码,请调用 GetExitCodeThread 函数。有关此函数的信息,请参见 Platform SDK。此函数获取线程(存储在 CWinThread 对象的 m_hThread 数据成员中)的句柄和 DWORD 的地址。如果线程仍然是活动的,GetExitCodeThread 将 STILL_ACTIVE 放置在提供的 DWO
10、RD 地址中;否则将退出代码放置在该地址中。检索 CWinThread 对象的退出代码还需要一步。默认情况下,当 CWinThread 线程终止时,删除该线程对象。这意味着不能访问 m_hThread 数据成员,因为 CWinThread 对象不再存在。若要避免出现这种情况,请执行以下操作之一:将 m_bAutoDelete 数据成员设置为 FALSE。这使 CWinThread 对象在线程终止后仍可以继续存在。然后可以在线程终止后,访问 m_hThread 数据成员。但是,如果使用此方法,就得销毁 CWinThread 对象,因为框架不会自动删除该对象。这是首选方法。单独存储线程的句柄。创建
11、线程后,(使用:DuplicateHandle)将其 m_hThread 数据成员复制到其他变量,并通过该变量访问该成员。这样,终止后即会自动删除对象,并且仍然可以找到线程终止的原因。请注意:在可以复制句柄之前,线程不终止。执行此操作的最安全的方式是将 CREATE_SUSPENDED 传递到 AfxBeginThread,存储句柄,然后通过调用 ResumeThread 继续执行线程。任一方法都可以使您确定 CWinThread 对象终止的原因。多线程处理:创建辅助线程 Visual Studio 2005 其他版本 辅助线程通常用于处理后台任务,用户不必等待即可继续使用应用程序。重新计算和
12、后台打印等任务是很好的辅助线程示例。本主题详细介绍创建辅助线程所需的步骤。主题包括:启动线程 实现控制函数 示例 创建辅助线程是一个相对较为简单的任务。只需两步即可以使线程运行:实现控制函数和启动线程。不必从 CWinThread 派生类。如果需要特殊版本的 CWinThread,可以从该类派生,但大多数简单辅助线程都不需要这样做。无需修改即可使用 CWinThread。启动线程 AfxBeginThread 有两个重载版本:一个用于用户界面线程,一个用于辅助线程。若要开始执行辅助线程,请调用 AfxBeginThread,并提供下列信息:控制函数的地址。要传递到控制函数的参数。(可选)所需的
13、线程优先级。默认值为正常优先级。有关可用的优先级级别的更多信息,请参见 Platform SDK 中的 SetThreadPriority。(可选)所需的线程堆栈大小。默认值与创建线程的堆栈大小相同。(可选)CREATE_SUSPENDED,如果希望在挂起状态中创建线程。默认值为 0,即正常启动线程。(可选)所需的安全属性。默认值与父线程具有相同的访问权。有关此安全信息格式的更多信息,请参见 Platform SDK 中的 SECURITY_ATTRIBUTES。AfxBeginThread 为您创建和初始化 CWinThread 对象、启动该对象并返回其地址,以便以后引用。在整个过程中进行检
14、查,确保假如创建过程的任何部分出现故障,所有对象都能被正确地解除分配。实现控制函数 控制函数定义线程。输入此函数后线程开始,此函数退出时线程终止。此函数的原型应为:复制 UINT MyControllingFunction(LPVOID pParam);该参数为单个值。函数在此参数中接收的值是在创建线程对象时传递到构造函数的值。控制函数可以用其选择的任何方式解释此值。它可以视为标量值,或指向包含多个参数的结构的指针,也可以忽略。如果参数引用结构,则既可以使用该结构将数据从调用方传递到线程,也可以用该结构将数据从线程传递回调用方。如果使用此类结构将数据传递回调用方,结果准备就绪时,线程需要通知调
15、用方。有关从辅助线程到调用方进行通信的信息,请参见多线程处理:编程提示。函数终止后,应该返回指示终止原因的 UINT 值。一般情况下,此退出代码为 0 指示成功;若为其他值,则指示不同类型的错误。这只依赖于实现。某些线程可以维护对象的使用计数,并返回该对象的当前使用数。有关应用程序如何检索此值的信息,请参见多线程处理:终止线程。在用 MFC 库编写的多线程程序中有一些操作限制。有关这些限制的说明以及使用线程的其他提示,请参见多线程处理:编程提示。控制函数示例 下面的示例演示如何定义控制函数,以及如何从程序的其他部分使用此函数。复制 UINT MyThreadProc(LPVOID pParam
16、)CMyObject*pObject=(CMyObject*)pParam;if(pObject=NULL|!pObject-IsKindOf(RUNTIME_CLASS(CMyObject)return 1;/if pObject is not valid /do something with pObject return 0;/thread completed successfully /inside a different function in the program.pNewObject=new CMyObject;AfxBeginThread(MyThreadProc,pNewOb
17、ject);多线程处理:如何使用同步类 Visual Studio 2005 其他版本 写入多线程应用程序时,线程间的同步资源访问是一个常见问题。两个或多个线程同时访问同一数据会导致不合需要的、不可预知的结果。例如,一个线程可能正在更新结构的内容,而另一个线程正在读取同一结构的内容。无法得知读取线程将会收到何种数据:旧数据、新写入的数据或两种数据都有。MFC 提供了多个同步和同步访问类以帮助解决此问题。本主题说明了可用的类以及如何在典型的多线程应用程序中使用它们创建线程安全类。典型的多线程应用程序具有代表各个线程间要共享的资源的类。正确设计的完全线程安全类不需要调用任何同步函数。该类的任何事情
18、都在内部处理,使您可以将精力集中于如何更好地使用类,而不是它如何会损坏。创建完全线程安全类的有效技术是将同步类合并到资源类中。将同步类合并到共享类是一个简单的过程。以维护链接的帐户列表的应用程序为例。此应用程序允许在独立的窗口中最多检查三个帐户,但是在任何特定的时间,只能更新一个帐户。更新帐户后,通过网络将更新的数据发送到数据存档。此示例应用程序使用所有这三种类型的同步类。因为它一次最多允许检查三个帐户,所以它使用 CSemaphore 限制对三个视图对象的访问。当试图查看第四个帐户时,应用程序或者等到前三个窗口中有一个关闭,或者该尝试失败。更新帐户时,应用程序使用 CCriticalSect
19、ion 确保一次只更新一个帐户。更新成功后,发出信号 CEvent 以释放等待该事件信号发送的线程。此线程将新数据发送到数据存档。设计线程安全类 若要使类完全线程安全,首先将适当的同步类作为数据成员添加到共享类中。在前面的帐户管理示例中,将 CSemaphore 数据成员添加到视图类,将 CCriticalSection 数据成员添加到链接的列表类,将 CEvent 数据成员添加到数据存储类。下一步,将同步调用添加到修改类中的数据或访问受控资源的所有成员函数中。应该在每个函数中创建 CSingleLock 或 CMultiLock 对象,并调用该对象的 Lock 函数。当锁定对象超出范围并被销
20、毁时,该对象的析构函数调用 Unlock 以释放资源。当然,如果愿意,可直接调用 Unlock。用这种方式设计线程安全类使得在多线程应用程序中使用该类与使用非线程安全类一样容易,但却具有更高的安全级别。将同步对象和同步访问权对象封装到资源的类将提供完全线程安全编程的所有优点,而不会有维护同步代码的缺点。下面的代码示例通过使用在共享资源类和 CSingleLock 对象中声明的数据成员 m_CritSection(CCriticalSection 类型),对此方法进行了说明。通过使用 m_CritSection 对象的地址创建 CSingleLock 对象,来试图同步共享资源(从 CWinThr
21、ead 派生)。试图锁定资源,一旦锁定,即完成了共享对象上的工作。完成工作后,即调用 Unlock 取消锁定资源。复制 CSingleLock singleLock(&m_CritSection);singleLock.Lock();/resource locked/.usage of shared resource.singleLock.Unlock();注意 与其他 MFC 同步类不同的是,CCriticalSection 没有计时锁定请求选项。等待释放线程的时间是无限的。此方法的缺点是类将要比没有添加同步对象的相同类慢一些。而且,如果有一个以上的线程可能删除对象,合并方法不一定始终有效。
22、在这种情况下,最好维持单独同步对象。有关确定在不同情况下使用何种同步类的信息,请参见 多线程编程:何时使用同步类。有关同步的更多信息,请参见“Platform SDK”中的 同步。有关 MFC 中的多线程支持的更多信息,请参见 使用 C+和 MFC 进行多线程编程。多线程处理:何时使用同步类 Visual Studio 2005 其他版本 MFC 提供的多线程类分为两类:同步对象(CSyncObject、CSemaphore、CMutex、CCriticalSection 和 CEvent)和同步访问对象(CMultiLock 和 CSingleLock)。当必须控制对资源的访问以确保资源的完
23、整性时,使用同步类。同步访问类用于获取对这些资源的访问权。本主题介绍各个类的适用情况。若要确定应使用的同步类,请询问以下一系列问题:1.应用程序必须等到发生某事才能访问资源(例如,在将数据写入文件之前,必须先从通信端口接收它)吗?如果是,请使用 CEvent。2.同一应用程序内一个以上的线程可以同时访问此资源(例如,应用程序允许在同一文档上最多同时打开五个带有视图的窗口)吗?如果是,请使用 CSemaphore。3.可以有一个以上的应用程序使用此资源(例如,资源在 DLL 中)吗?如果是,请使用 CMutex。如果不是,请使用 CCriticalSection。从不直接使用 CSyncObje
24、ct。它是其他四个同步类的基类。示例 1:使用三个同步类 以维护链接的帐户列表的应用程序为例。此应用程序允许在独立的窗口中最多检查三个帐户,但是在任何特定的时间,只能更新一个帐户。更新帐户后,通过网络将更新的数据发送到数据存档。此示例应用程序使用所有这三种类型的同步类。因为它一次最多允许检查三个帐户,因此使用 CSemaphore 限制对三个视图对象的访问。当试图查看第四个帐户时,应用程序或者等到前三个窗口中有一个关闭,或者该尝试失败。更新帐户时,应用程序使用 CCriticalSection 确保一次只更新一个帐户。更新成功后,发出信号 CEvent 以释放等待该事件信号发送的线程。此线程将
25、新数据发送到数据存档。示例 2:使用同步访问类 选择要使用的同步访问类更为简单。如果应用程序只与访问单个受控资源有关,请使用 CSingleLock。如果需要访问多个受控资源中的任何一个,则使用 CMultiLock。在示例 1 中,应使用 CSingleLock,因为在每种情况下,任何特定时间都只需要一个资源。有关如何使用同步类的信息,请参见多线程处理:如何使用同步类。有关同步的信息,请参见 Platform SDK 中的同步。有关 MFC 中多线程处理支持的信息,请参见使用 C+和 MFC 进行多线程处理。多线程处理:编程提示 Visual Studio 2005 其他版本 访问数据时,使
26、用多线程应用程序比使用单线程应用程序要更加小心。因为在多线程应用程序中同时有多个独立的执行路径正在使用,算法或数据或两者都必须注意:可以有一个以上的线程同时使用数据。本主题说明在使用 Microsoft 基础类(MFC)库编制多线程应用程序时避免发生潜在问题的技术。从多线程访问对象 从非 MFC 线程访问 MFC 对象 Windows 句柄映射 线程间通信 从多线程访问对象 由于大小和性能原因,MFC 对象在对象级别不是线程安全的,而只是在类级别线程安全。这表明可以有两个独立的线程操作两个不同的 CString 对象,但不能有两个线程操作同一个 CString 对象。如果一定要有多个线程操作同
27、一个对象,请用适当的 Win32 同步机制(如临界区)保护此类访问权。有关临界区和其他相关对象的更多信息,请参见 Platform SDK 中的同步。类库内部使用临界区以保护全局数据结构,例如调试存储分配使用的结构。从非 MFC 线程访问 MFC 对象 如果多线程应用程序使用 CWinThread 对象以外的方式创建线程,则不能从该线程访问其他 MFC 对象。换句话说,如果要从次要线程访问任何 MFC 对象,则必须用多线程编程:创建用户界面线程或多线程编程:创建辅助线程中所述的方法之一创建该线程。只有这些方法才允许类库初始化处理多线程应用程序所需的内部变量。Windows 句柄映射 作为通用规
28、则,线程只能访问它创建的 MFC 对象。这是因为临时和永久性 Windows 句柄映射保留在线程本地存储中,以对它进行保护,确保不能有多个线程同时访问它。例如,辅助线程不能执行计算并调用文档的 UpdateAllViews 成员函数来修改包含新数据视图的窗口。此操作将不会有任何效果,因为从 CWnd 对象到 HWND 的映射是主线程的本地映射。这意味着一个线程可能有从 Windows 句柄到 C+对象的映射,但是另一个线程可能会将此句柄映射到其他 C+对象。在一个线程内所做的更改将不会反映在另一个线程中。有几种方式可以避免此问题。首先是将各个句柄(如 HWND)而不是 C+对象传递到辅助线程。
29、然后,辅助线程通过调用适当 FromHandle 成员函数将这些对象添加到它的临时映射。还可以通过调用 Attach 将对象添加到线程的永久映射,但只有在保证对象比线程存在的时间长时,才应当进行此操作。另一个方法是创建新的与辅助线程将要执行的不同任务相对应的用户定义消息,并使用:PostMessage 将这些消息发布到应用程序的主窗口。此通信方法类似于两个不同的应用程序在对话,只不过这两个线程在同一地址空间中执行。有关句柄映射的更多信息,请参见 技术说明 3。有关线程本地存储的更多信息,请参见 Platform SDK 中的 线程本地存储 和 使用线程本地存储。线程间通信 MFC 提供了多个类
30、,使线程可以同步访问对象以维护线程安全。多线程编程:如何使用同步类 和 多线程编程:何时使用同步类 中描述了这些类的用法。有关这些对象的更多信息,请参见 Platform SDK 中的 同步。多线程概述 进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说 main 或 WinMain 函数,将程序的启动点提供给 Wi
31、ndows 系统。主执行线程终止了,进程也就随之终止。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。多线程可以实现并行处理,避免了某项任务长时间占用 CPU 时间。要说明的一点是,目前大多数的计算机都是单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些 CPU 时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象
32、这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对 CPU 的控制权,在线程切换时会消耗很多的 CPU 资源,反而会降低系统的性能。这一点在多线程编程时应该注意。Win32 SDK 函数支持进行多线程的程序设计,并提供了操作系统原理中的各种同步、互斥和临界区等操作。Visual C+6.0 中,使用 MFC 类库也实现了多线程的程序设计,使得多线程编程更加方便。Win32 API 对多线程编程的支持 Win32 提供了一系列的 API 函数来完成线程的创建、挂起、恢复、终结以及通信等工作。下面将选取其中的一些重要函数进行说明。1、HANDLE CreateThread(LPSEC
33、URITY_ATTRIBUTES lpThreadAttributes,DWORD dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId);该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄,其中各参数说明如下:lpThreadAttributes:指向一个 SECURITY_ATTRIBUTES 结构的指针,该结构决定了线程的安全属性,一般置为 NULL;dwStackSize:指定了线程的堆栈深度,一般都设置
34、为 0;lpStartAddress:表示新线程开始执行时代码所在函数的地址,即线程的起始地址。一般情况为(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc 是线程函数名;lpParameter:指定了线程执行时传送给线程的 32 位参数,即线程函数的参数;dwCreationFlags:控制线程创建的附加标志,可以取两种值。如果该参数为 0,线程在被创建后就会立即开始执行;如果该参数为 CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数 ResumeThread 被调用;lpThreadId:该参数返回所创建
35、线程的 ID;如果创建成功则返回线程的句柄,否则返回 NULL。2、DWORD SuspendThread(HANDLE hThread);该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。3、DWORD ResumeThread(HANDLE hThread);该函数用于结束线程的挂起状态,执行线程。4、VOID ExitThread(DWORD dwExitCode);该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。其中参数 dwExitCode用来设置线程的退出码。5、BOOL TerminateThread(HANDLE hThread,DWORD dwExi
36、tCode);一般情况下,线程运行结束之后,线程函数正常返回,但是应用程序可以调用TerminateThread 强行终止某一线程的执行。各参数含义如下:hThread:将被终结的线程的句柄;dwExitCode:用于指定线程的退出码。使用 TerminateThread()终止某个线程的执行是不安全的,可能会引起系统不稳定;虽然该函数立即终止线程的执行,但并不释放线程所占用的资源。因此,一般不建议使用该函数。6、BOOL GetExitCodeThread(HANDLE hThread,/handle to the thread LPDWORD lpExitCode/address to r
37、eceive termination status );得到终止线程状态,如果状态为 STILL_ACTIVE,线程没有终止,否则线程终止。7、BOOL PostThreadMessage(DWORD idThread,UINT Msg,WPARAM wParam,LPARAM lParam);该函数将一条消息放入到指定线程的消息队列中,并且不等到消息被该线程处理时便返回。idThread:将接收消息的线程的 ID;Msg:指定用来发送的消息;wParam:同消息有关的字参数;lParam:同消息有关的长参数;调用该函数时,如果即将接收消息的线程没有创建消息循环,则该函数执行失败。注:没有对应
38、 SendThreadMessage 函数,因为 SendMessage 是不安全的,发送消息到一个窗口,自己等待,消息处理完成之后返回。如果消息始终没有处理完成返回的话,就会存在死锁问题,所以线程中没有对应 SendThreadMessage 之类的函数。SendMessag、PostMessage、GetMessage、PeekMessage 区别 SendMessag 是发送消息到另一个窗口,自己等待,消息处理完成之后返回。(表面上另一个窗口消息处理是自己窗口来执行完成的,其实另一个窗口消息处理真正的执行者是SendMessag 这个窗口)PostMessage 是发送消息到消息队列中,
39、自己马上返回。GetMessage 消息过滤,等到有合适的消息时才返回,同时会将消息从队列中删除。PeekMessage 消息过滤,查看了一下消息队列,PeekMessage 可以设置最后一个参数wRemoveMsg 来决定是否将消息保留在队列中。MFC 对多线程编程的支持 MFC 中有两类线程,分别称之为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消
40、息等。但对于 Win32 的 API 编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。在 MFC 中,一般用全局函数 AfxBeginThread()来创建并初始化一个线程的运行,该函数有两种重载形式,分别用于创建工作者线程和用户界面线程。两种重载函数原型和参数分别说明如下:(1)CWinThread*AfxBeginThread(AFX_THREADPROC pfnThreadProc,LPVOID pParam,nPriority=THREAD_PRIORITY_NORMAL,UINT nStackSize=0,DWORD dwCreateFlags=0,
41、LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);PfnThreadProc:指向工作者线程的执行函数的指针,线程函数原型必须声明如下:UINT ExecutingFunction(LPVOID pParam);请注意,ExecutingFunction()应返回一个 UINT 类型的值,用以指明该函数结束的原因。一般情况下,返回 0 表明执行成功。pParam:传递给线程函数的一个 32 位参数,执行函数将用某种方式解释该值。它可以是数值,或是指向一个结构的指针,甚至可以被忽略;nPriority:线程的优先级。如果为 0,则线程与其父线程具有相同的优先
42、级;nStackSize:线程为自己分配堆栈的大小,其单位为字节。如果 nStackSize 被设为 0,则线程的堆栈被设置成与父线程堆栈相同大小;dwCreateFlags:如果为 0,则线程在创建后立刻开始执行。如果为 CREATE_SUSPEND,则线程在创建后立刻被挂起;lpSecurityAttrs:线程的安全属性指针,一般为 NULL;(2)CWinThread*AfxBeginThread(CRuntimeClass*pThreadClass,int nPriority=THREAD_PRIORITY_NORMAL,UINT nStackSize=0,DWORD dwCreate
43、Flags=0,LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);pThreadClass 是指向 CWinThread 的一个导出类的运行时类对象的指针,该导出类定义了被创建的用户界面线程的启动、退出等;其它参数的意义同形式 1。使用函数的这个原型生成的线程也有消息机制,在以后的例子中我们将发现同主线程的机制几乎一样。下面我们对 CWinThread 类的数据成员及常用函数进行简要说明。m_hThread:当前线程的句柄;m_nThreadID:当前线程的 ID;m_pMainWnd:指向应用程序主窗口的指针.线程间通讯 一般而言,应用程序中的一个次要线
44、程总是为主线程执行特定的任务,这样,主线程和次要线程间必定有一个信息传递的渠道,也就是主线程和次要线程间要进行通信。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的,下面将进行说明。使用全局变量进行通信 由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,我们建议使用 volatile 修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。如果线程间所需传递的信息较复杂,我们可以定义一个结构,通过传递指向该结构的指针进行传递信息。使用自定义消息 我们可以在
45、一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。利用 Windows 操作系统的消息驱动机制,当一个线程发出一条消息时,操作系统首先接收到该消息,然后把该消息转发给目标线程,接收消息的线程必须已经建立了消息循环。线程的同步 虽然多线程能给我们带来好处,但是也有不少问题需要解决。例如,对于像磁盘驱动器这样独占性系统资源,由于线程可以执行进程的任何代码段,且线程的运行是由系统调度自动完成的,具有一定的不确定性,因此就有可能出现两个线程同时对磁盘驱动器进行操作,从而出现操作错误。使隶属于同一进程的各线程协调一致地工作称为线程的同步
46、。MFC 提供了多种同步对象,下面我们只介绍最常用的四种:临界区(CCriticalSection)事件(CEvent)互斥量(CMutex)信号量(CSemaphore)A、使用 CCriticalSection 类 当多个线程访问一个独占性共享资源时,可以使用“临界区”对象。任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其他希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。CCriticalSection 类的用法非常简单,步骤如下:定义 CCriticalSection
47、类的一个全局对象(以使各个线程均能访问),如 CCriticalSection critical_section;在访问需要保护的资源或代码之前,调用 CCriticalSection 类的成员 Lock()获得临界区对象:critical_section.Lock();在线程中调用该函数来使线程获得它所请求的临界区。如果此时没有其它线程占有临界区对象,则调用 Lock()的线程获得临界区;否则,线程将被挂起,并放入到一个系统队列中等待,直到当前拥有临界区的线程释放了临界区时为止。访问临界区完毕后,使用 CCriticalSection 的成员函数 Unlock()来释放临界区:critica
48、l_section.Unlock();再通俗一点讲,就是线程 A 执行到 critical_section.Lock();语句时,如果其它线程(B)正在执行critical_section.Lock();语句后且 critical_section.Unlock();语句前的语句时,线程 A 就会等待,直到线程 B 执行完 critical_section.Unlock();语句,线程 A 才会继续执行。B、使用 CEvent 类 CEvent 类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为 A)负责监听通讯端口
49、,另外一个线程(记为 B)负责更新用户数据。通过使用 CEvent 类,线程 A 可以通知线程 B 何时更新用户数据。每一个 CEvent 对象可以有两种状态:有信号状态和无信号状态。线程监视位于其中的 CEvent 类对象的状态,并在相应的时候采取相应的操作。在 MFC 中,CEvent 类对象有两种类型:人工事件和自动事件。一个自动 CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放可利用线程,但直到调用成员函数 ReSetEvent()才将其设置为无信号状态。在创建 CEvent 类的对象时,默认创建的是自动事件。CEvent 类的各成员函数的
50、原型和参数说明如下:1、CEvent(BOOL bInitiallyOwn=FALSE,BOOL bManualReset=FALSE,LPCTSTR lpszName=NULL,LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);bInitiallyOwn:指定事件对象初始化状态,TRUE 为有信号,FALSE 为无信号;bManualReset:指定要创建的事件是属于人工事件还是自动事件。TRUE 为人工事件,FALSE为自动事件;后两个参数一般设为 NULL,在此不作过多说明。2、BOOL CEvent:SetEvent();将 CEvent 类对象的状态