Visual C++ 5.0中的多线程编程技术

Visual C++ 5.0 中的多线程编程技术
一、引言

   Windows 系统平台经历了从 16 位到 32 位的转变后,系统运行方式和任务管理方式有了很大的变化,在 Windows 95 Windows NT 中,每个 Win32 程序在独立的进程空间上运行, 32 位地址空间使我们从 16 位段式结构的 64K 段限制中摆脱出来,逻辑上达到了 4G 的线性地址空间。这样,我们在设计程序时就不再需要考虑编译的段模式,同时还提高了大程序的运行效率。独立进程空间的另一个更大的优越性是大大提高了系统的稳定性,一个应用程序的异常错误不会影响其它的应用程序,这对于现在的桌面环境尤为重要。

  在 Windows 的一个进程内,包含一个或多个线程。线程是指进程的一条执行路径,它包含独立的堆栈和 CPU 寄存器状态,每个线程共享所有的进程资源,包括打开的文件、信号标识及动态分配的内存等等。一个进程内的所有线程使用同一个 32 位地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行以及什么时候执行线程。线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完任务后再执行。在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样就可使处理器的任务平衡,也提高了系统的运行效率。

   32 Windows 环境下的 Win32 API 提供了多线程应用程序开发所需要的接口函数,但 Win16 Win32 对多线程应用并不支持,利用 Visual C++ 5.0 中提供的标准 C 库也可以开发多线程应用程序,而相应的 MFC4.21 类库则封装了多线程编程的类,因而用户在开发时可根据应用程序的需要和特点选择相应的工具。

  如果用户的应用程序需要有多个任务同时进行相应的处理,则使用多线程是较理想的选择。例如,就网络文件服务功能的应用程序而言,若采用单线程编程方法,则需要循环检查网络的连接、磁盘驱动器的状况,并在适当的时候显示这些数据,必须等到一遍查询后才能刷新数据的显示。对使用者来说,延迟可能很长。而在应用多线程的情况下可将这些任务分给多个线程,一个线程负责检查网络,另一个线程管理磁盘驱动器,还有一个线程负责显示数据,三个线程结合起来共同完成文件服务,使用者也可以及时看到网络的变化。多线程应用范围很广,尤其是在目前的桌面平台上,系统的许多功能如网络 (Internet) 、打印、字处理、图形图像、动画和文件管理都在一个系统下运行,更需要我们的应用程序能够同时处理多个事件,而这些正是多线程可以实现的。本文讲述了利用 Visual C++ 5.0 进行多线程开发的编程技术。

  二、基于 Visual C++ 的多线程编程

   Visual C++ 5.0 提供了 Windows 应用程序的集成开发环境 Developer Studio 。在这个环境里,用户既可以编写 C 风格的 32 Win32 应用程序,也可以利用 MFC 类库编写 C++ 风格的应用程序,二者各有其优点:基于 Win32 的应用程序执行代码小巧,运行效率高,但要求程序员编写的代码较多,且需要管理所有系统提供给程序的资源;而基于 MFC 类库的应用程序可以快速建立起应用程序,类库为程序员提供了大量的封装类,而且 Developer Studio 为程序员提供了一些工具来管理用户源程序,其缺点是类库代码很庞大,应用程序的执行代码离不开这些代码。由于使用类库所带来的快速、简捷和功能强大等优越性,因此,除非有特殊的需要,否则 Visual C++ 提倡使用 MFC 类库进行应用程序开发。

  多线程的编程在 Win32 方式下和 MFC 类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程。当线程执行完任务后,自动中止线程;当进程结束后,所有的线程都中止。所有活动的线程共享进程的资源。因此,在编程时需要考虑在多个线程访问同一资源时产生冲突的问题:当一个线程正在访问一个进程对象时,另一个线程要改变该对象,这时可能会产生错误的结果。所以,程序员编程时要解决这种冲突。

  下面给大家介绍一下在 Win32 基础上进行多线程编程的过程。

   1. Win32 函数创建和中止线程

   Win32 函数库中提供了多线程控制的操作函数,包括创建线程、中止线程、建立互斥区等。首先,在应用程序的主线程或者其它活动线程的适当地方创建新的线程。创建线程的函数如下:

   HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );

  其中,参数 lpThreadAttributes 指定了线程的安全属性,在 Windows 95 中被忽略; dwStackSize 指定了线程的堆栈深度; lpStartAddress 指定了线程的起始地址,一般情况为下面的原型函数: DWORD WINAPI ThreadFunc( LPVOID );lpParameter 指定了线程执行时传送给线程的 32 位参数,即上面函数的参数; dwCreationFlags 指定了线程创建的特性; lpThreadId 指向一个 DWORD 变量,可返回线程 ID 值。

  如果创建成功则返回线程的句柄,否则返回 NULL

  创建了新的线程后,则该线程就开始启动执行了。如果在 dwCreationFlags 中用了 CREATE_SUSPENDED 特性,那么线程并不马上执行,而是先挂起,等到调用 ResumeThread 后才开始启动线程,在这个过程中可以调用函数:

   BOOL SetThreadPriority( HANDLE hThread, int nPriority);

  来设置线程的优先权。

  当线程的函数返回后,线程自动中止。如果在线程的执行过程中中止的话,则可调用函数:

   VOID ExitThread( DWORD dwExitCode);

  如果在线程的外面中止线程的话,则可调用下面的函数:

   BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );

  但应注意:该函数可能会引起系统不稳定,而且线程所占用的资源也不释放。因此,一般情况下,建议不要使用该函数。

  如果要中止的线程是进程内的最后一个线程,则在线程被中止后相应的进程也应中止。

   2. Win32 函数控制线程对共享资源的访问

  在线程体内,如果该线程完全独立,与其它的线程没有数据存取等资源操作上的冲突,则可按照通常单线程的方法进行编程。但是,在多线程处理时情况常常不是这样,线程之间经常要同时访问一些资源。例如,一个线程负责公式计算,另一个线程负责结果的显示,两个线程都要访问同一个结果变量。这时如果不进行冲突控制的话,则很可能显示的是不正确的结果。

  对共享资源进行访问引起冲突是不可避免的,但我们可用以下办法来进行操作控制:

   (1) 通过设置线程的互斥体对象,在可能冲突的地方进行同步控制。

  首先,建立互斥体对象,得到句柄:

   HANDLE CreateMutex( );

  然后,在线程可能冲突区域的开始(即访问共享资源之前),调用 WaitForSingleObject 将句柄传给函数,请求占用互斥体对象:

   dwWaitResult = WaitForSingleObject(hMutex, 5000L);

  共享资源访问完后,释放对互斥体对象的占用:

   ReleaseMutex(hMutex);

  互斥体对象在同一时刻只能被一个线程占用。当互斥体对象被一个线程占用时,若有另一线程想占用它,则必须等到前一线程释放后才能成功。

   (2) 设置信号:在操作共享资源前,打开信号;完成操作后,关闭信号。这类似于互斥体对象的处理。

  首先,创建信号对象:

   HANDLE CreateSemaphore( );

  或者打开一个信号对象:

   HANDLE OpenSemaphore( );

  然后,在线程的访问共享资源之前调用 WaitForSingleObject

  共享资源访问完后,释放对信号对象的占用:

   ReleaseSemaphore();

  信号对象允许同时对多个线程共享资源的访问,在创建对象时指定最大可同时访问的线程数。当一个线程申请访问成功后,信号对象中的计数器减一;调用 ReleaseSemaphore 函数后,信号对象中的计数器加一。其中,计数器值大于等于 0 ,小于等于创建时指定的最大值。利用信号对象,我们不仅可以控制共享资源的访问,还可以在应用的初始化时候使用。假定一个应用在创建一个信号对象时,将其计数器的初始值设为 0 ,这样就阻塞了其它线程,保护了资源。待初始化完成后,调用 ReleaseSemaphore 函数将其计数器增加至最大值,进行正常的存取访问。

   (3) 利用事件对象的状态,进行线程对共享资源的访问。

  用 ResetEvent 函数设置事件对象状态为不允许线程通过;用 SetEvent 函数设置事件对象状态为可以允许线程通过。

  事件分为手工释放和自动释放。如果是手工释放,则按照上述两函数处理事件的状态;如果是自动释放,则在一个线程结束后,自动清除事件状态,允许其它线程通过。

   (4) 设置排斥区。在排斥区中异步执行时,它只能在同一进程的线程之间共享资源处理。虽然此时上面介绍的三种方法均可使用,但是,使用排斥区的方法则使同步管理的效率更高;

  先定义一个 CRITICAL_SECTION 结构的排斥区对象,在进程使用之前先对对象进行初始化,调用如下函数:

   VOID InitializeCriticalSection( LPCRITICAL_SECTION );

  当一个线程使用排斥区时,调用函数:

   EnterCriticalSection 或者 TryEnterCriticalSection

  当要求占用、退出排斥区时,调用函数:

   LeaveCriticalSection

  释放对排斥区对象的占用,供其它线程使用。

  互斥体对象、信号对象和事件对象也可以用于进程间的线程同步操作。在用 Win32 函数创建了对象时,我们可以指定对象的名字,还可以设置同步对象在子进程的继承性。创建返回的是 HANDLE 句柄,我们可以用函数 DuplicateHandle 来复制对象句柄,这样每个进程都可以拥有同一对象的句柄,实现进程之间的线程同步操作。另外,在同一进程内,我们可以用 OpenMutex OpenSemaphore OpenEvent 来获得指定名字的同步对象的句柄。

  排斥区异步执行的线程同步方法只能用于同一进程的线程之间共享资源处理,但是这种方法的使用效率较高,而且编程也相对简单一些。

  在 Visual C++ 中,除了利用 Win32 函数进行多线程同步控制外,如果我们用到了 MFC 类库,则可利用已经封装成 C++ 类结构的同步对象,使我们的编程更加简捷。

  三、基于 MFC 的多线程编程

  在 Visual C++ 5.0 附带的 MFC 4.21 类库中,也提供了多线程编程的支持,基本原理与上面所讲的基于 Win32 函数的设计一致,但由于 MFC 对同步对象作了封装,因此对用户编程实现来说更加方便,避免了对象句柄管理上的繁琐工作。更重要的是,在多个窗口线程情况下, MFC 中直接提供了用户接口线程的设计。

  在 MFC 中,线程分为两种:用户接口线程和辅助线程。用户接口线程常用于接收用户的输入,处理相应的事件和消息。在用户接口线程中,包含一个消息处理循环,其中 CWinApp 就是一个典型的例子,它从 CWinThread 派生出来,负责处理用户输入产生的事件和消息。辅助线程常用于任务处理(比如计算)不要求用户输入,对用户而言,它在后台运行。 Win32 API 并不区分这两种线程的类型,它只是获取线程的起始地址,然后开始执行线程。而 MFC 则针对不同的用户需要作了分类。如果我们需要编写多个有用户接口的线程的应用程序,则利用 Win32 API 要写很多的框架代码来完成每个线程的消息事件的处理,而用 MFC 则可以充分发挥 MFC 中类的强大功能,还可以使用 ClassWizard 来帮助管理类的消息映射和成员变量等,我们就可以把精力集中到应用程序的相关代码编写上。

  辅助线程编程较为简单,设计的思路与上节所讲的基本一致:一个基本函数代表了一个线程,创建并启动线程后,则线程进入运行状态;如果线程用到共享资源,则需要进行资源同步处理。共享资源的同步处理在两种线程模式下完全一致。

  我们知道:基于 MFC 的应用程序有一个应用对象,它是 CWinApp 派生类的对象,该对象代表了应用进程的主线程。当线程执行完 ( 通常是接收到 WM_QUIT 消息 ) 并退出线程时,由于进程中没有其它线程的存在,故进程也自动结束。类 CWinApp CWinThread 派生出来, CWinThread 是用户接口线程的基本类。我们在编写用户接口线程时,需要从 CWinThread 派生我们自己的线程类, ClassWizard 可以帮助我们完成这个工作。

  下面列出编写用户接口线程的基本步骤。

   1. ClassWizard 派生一个新的类,设置基类为 CWinThread

   2. 创建线程并启动线程

  可以用两种方法来创建用户接口线程。

  ( 1 MFC 提供了两个版本的 AfxBeginThread 函数,其中一个用于创建用户接口线程,函数原型如下:

   CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority, UINT nStackSize , DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs );

  其中,参数 pThreadClass 指定线程的运行类,函数返回线程对象。

  在创建线程时,可以指定线程先挂起,将参数 dwCreateFlags 设置为 CREATE_SUSPENDED 。然后,做一些初试工作,如对变量赋值等。最后,再调用线程类的 ResumeThread 函数启动线程。

  函数 AfxBeginThread 的另一个版本指定一个线程函数并设置相应的参数,其它设置及用法与上述函数基本相同。

  ( 2 )我们也可以不用 AfxBeginThread 创建线程,而是分两步完成:首先,调用线程类的构造函数创建一个线程对象;其次,调用 CWinThread::CreateThread 函数来创建该线程。

  注意:在这种情况下,在线程类中需要有公有的构造函数以创建其相应的 C++ 对象。

  线程建立并启动后,则线程在线程函数执行过程中一直有效。如果是线程对象,则在对象被删除之前,先结束线程。 CWinThread 已经为我们完成了线程结束的工作。

   3. 同步对象的使用

  不管是辅助线程还是用户接口线程,在存取共享资源时,都需要保护共享资源,以免引起冲突,造成错误。处理方法类似于 Win32 API 函数的使用,但 MFC 为我们提供了几个同步对象 C++ 类,即 CSyncObject CMutex CSemaphore CEvent CCriticalSection 。这里, CSyncObject 为其它四个类的基类,后四个类分别对应前面所讲的四个 Win32 API 同步对象。

  通常,我们在 C++ 对象的成员函数中使用共享资源,或者把共享资源封装在 C++ 类的内部。我们可将线程同步操作封装在对象类的实现函数当中,这样在应用中的线程使用 C++ 对象时,就可以像一般对象一样使用它,简化了使用部分代码的编写,这正是面向对象编程的思想。这样编写的类被称作 线程安全类 。在设计线程安全类时,首先应根据具体情况在类中加入一个同步对象类数据成员。然后,在类的成员函数中,凡是所有修改公共数据或者读取公共数据的地方均要加入相应的同步调用。一般的处理步骤是:创建一个 CSingleLock 或者 CMultiLock 对象,然后调用其 Lock 函数。当对象结束时,自动在析构函数中调用 Unlock 函数,当然也可以在任何希望的地方调用 Unlock 函数。

  如果不是在特定的 C++ 对象中使用共享资源,而是在特定的函数中使用共享资源(这样的函数称为 线程安全函数 ),那么还是按照前面介绍的办法去做:先建立同步对象,然后调用等待函数,直到可以访问资源,最后释放对同步对象的控制。

  下面我们讨论四个同步对象分别适用的场合:

  ( 1 )如果某个线程必须等待某些事件发生后才能存取相应资源,则用 CEvent

  ( 2 )如果一个应用同时可以有多个线程存取相应资源,则用 CSemaphore

  ( 3 )如果有多个应用 ( 多个进程 ) 同时存取相应资源,则用 CMutex ,否则用 CCriticalSection

  使用线程安全类或者线程安全函数进行编程,比不考虑线程安全的编程要复杂,尤其在进行调试时情况更为复杂,我们必须灵活使用 Visual C++ 提供的调试工具,以保证共享资源的安全存取。线程安全编程的另一缺点是运行效率相对要低些,即使在单个线程运行的情况下也会损失一些效率。所以,我们在实际工作中应具体问题具体分析,以选择合适的编程方法。

   4. 多线程编程例程分析

  上面讲述了在 Visual C++ 5.0 中进行多线程编程的技术要点,为了充分说明这种技术,我们来分析一下 Visual C++ 提供的有关多线程的例程,看看一些多线程元素的典型用法。读者可运行这些例程,以获得多线程运行的直观效果。

  ( 1 MtRecalc

  例程 MtRecalc 的功能是在一个窗口中完成简单的加法运算,用户可输入加数和被加数,例程完成两数相加。用户可通过菜单选择单线程或用辅助线程来做加法运算。如果选择辅助线程进行加法运算,则在进行运算的过程中,用户可继续进行一些界面操作,如访问菜单、编辑数值等,甚至可以中止辅助运算线程。为了使其效果更加明显,例程在计算过程中使用了循环和延时,模拟一个复杂的计算过程。

  在程序的 CRecalcDoc 类中,用到了一个线程对象和四个同步事件对象:

   CWinThread* m_pRecalcWorkerThread;

   HANDLE m_hEventStartRecalc;

   HANDLE m_hEventRecalcDone;

   HANDLE m_hEventKillRecalcThread;

   HANDLE m_hEventRecalcThreadKilled;

  当用户选择了菜单项 Worker Thread 后,多线程功能才有效。这时,或者选择菜单项 Recalculate Now ,或者在窗口中的编辑控制转移焦点时,都会调用函数:

   void CRecalcDoc::UpdateInt1AndInt2(int n1, int n2, BOOL bForceRecalc);

  在多线程的情况下,还会调用下面的 CRecalcDoc::RecalcInSecondThread 函数:

   void CRecalcDoc::RecalcInSecondThread()

   {

   if (m_pRecalcWorkerThread == NULL)

   {

   m_pRecalcWorkerThread =

   AfxBeginThread(RecalcThreadProc, &m_recalcThreadInfo);

   }

   m_recalcThreadInfo.m_nInt1 = m_nInt1;

   m_recalcThreadInfo.m_nInt2 = m_nInt2;

   POSITION pos = GetFirstViewPosition();

   CView* pView = GetNextView(pos);

   m_recalcThreadInfo.m_hwndNotifyRecalcDone = pView->m_hWnd;

   m_recalcThreadInfo.m_hwndNotifyProgress = AfxGetMainWnd()->m_hWnd;

   m_recalcThreadInfo.m_nRecalcSpeedSeconds = m_nRecalcSpeedSeconds;

   SetEvent(m_hEventRecalcDone);

   ResetEvent(m_hEventKillRecalcThread);

   ResetEvent(m_hEventRecalcThreadKilled);

   SetEvent(m_hEventStartRecalc);

   }

  上面加粗的语句是与多线程直接相关的代码,应用程序调用 AfxBeginThread 启动了线程,把 m_recalcThreadInfo 作为参数传给线程函数。函数中最后的四行语句设置了四个事件对象的状态,这些事件对象在文档类的构造函数中创建。下面是实际的运算线程函数:

   UINT RecalcThreadProc(LPVOID pParam)

   {

   CRecalcThreadInfo* pRecalcInfo = (CRecalcThreadInfo*)pParam;

   BOOL bRecalcCompleted;

   while (TRUE)

   {

   bRecalcCompleted = FALSE;

   if (WaitForSingleObject(pRecalcInfo->m_hEventStartRecalc, INFINITE)

   != WAIT_OBJECT_0)

   break;

   if (WaitForSingleObject(pRecalcInfo->m_hEventKillRecalcThread, 0)

   WAIT_OBJECT_0)

   break; // Terminate this thread by existing the proc.

   ResetEvent(pRecalcInfo->m_hEventRecalcDone);

   bRecalcCompleted = SlowAdd(pRecalcInfo->m_nInt1,

   pRecalcInfo->m_nInt2,

   pRecalcInfo->m_nSum,

   pRecalcInfo,

   pRecalcInfo->m_nRecalcSpeedSeconds,

   pRecalcInfo->m_hwndNotifyProgress);

   SetEvent(pRecalcInfo->m_hEventRecalcDone);

   if (!bRecalcCompleted) // If interrupted by kill then...

   break; // terminate this thread by exiting the proc.

   ::PostMessage(pRecalcInfo->m_hwndNotifyRecalcDone, WM_USER_RECALC_DONE, 0, 0);

   }

   if (!bRecalcCompleted)

   SetEvent(pRecalcInfo->m_hEventRecalcThreadKilled);

   return 0;

   }

  

   BOOL SlowAdd(int nInt1, int nInt2, int& nResult, CRecalcThreadInfo* pInfo, int nSeconds,HWND hwndNotifyProgress)

   {

   CWnd* pWndNotifyProgress = CWnd::FromHandle(hwndNotifyProgress);

   BOOL bRestartCalculation = TRUE;

   while (bRestartCalculation)

   {

   bRestartCalculation = FALSE;

   for (int nCount = 1; nCount <20; nCount++)

   {

   if (pInfo != NULL

   && WaitForSingleObject(pInfo->m_hEventKillRecalcThread, 0) == WAIT_OBJECT_0)

   {

   if (hwndNotifyProgress != NULL)

   {

   pWndNotifyProgress->PostMessage( WM_USER_RECALC_IN_PROGRESS);

   }

   return FALSE; // Terminate this recalculation

   }

   if (pInfo != NULL

   &&WaitForSingleObject(pInfo->m_hEventStartRecalc, 0) == WAIT_OBJECT_0)

   {

   nInt1 = pInfo->m_nInt1;

   nInt2 = pInfo->m_nInt2;

   bRestartCalculation = TRUE;

   continue;

   }

   // update the progress indicator

   Sleep(nSeconds * 50);

   }

   // update the progress indicator

   }

   nResult = nInt1 + nInt2;

   return TRUE;

   }

  上面的代码充分显示了几个事件对象的用法。当线程刚启动时,先等待 m_hEventStartRecalc 的状态为允许,然后检查 m_hEventKillRecalcThread 事件对象的状态。注意这两个等待函数调用的第二个参数的区别:在进入计算函数之前,设置 m_hEventRecalcDone 事件为不允许状态;待计算结束后,将其设置为允许状态。在计算函数的处理过程中,循环检查事件 m_hEventKillRecalcThread m_hEventStartRecalc 的状态,如果 m_hEventKillRecalcThread 事件允许,则退出线程,中止计算。

  当计算线程在计算时,主线程可继续接受用户输入(包括菜单选择)。用户可通过菜单项中止计算线程。中止线程的处理比较简单,代码如下:

   void CRecalcDoc::OnKillWorkerThread()

   {

   SetEvent(m_hEventKillRecalcThread);

   SetEvent(m_hEventStartRecalc);

   WaitForSingleObject(m_hEventRecalcThreadKilled, INFINITE);

   m_pRecalcWorkerThread = NULL;

   m_bRecalcInProgress = FALSE; // but m_bRecalcNeeded is still TRUE

   UpdateAllViews(NULL, UPDATE_HINT_SUM);

   }

  通过设置 m_hEventKillRecalcThread 事件对象,计算线程的循环就会检测到该事件的状态,最终引起线程的退出。注意:线程的中止因函数的退出而自然中止,而没有用强行办法中止,这样可保证系统的安全性。另外,在程序的很多地方使用了 PostMessage 来更新计算进度的指示,使用 PostMessage 函数发送消息可立即返回,无需等待,这样就避免了阻塞,比较符合多线程编程的思想,建议读者使用这种消息发送方法。尤其是在多个 UI 线程编程时,用这种方法更合适。

  ( 2 MtMDI

   例程 MtMDI 是一个 MDI 应用,每一个子窗口是一个用户接口线程,子窗口里有一个来回弹跳的小球,小球的运动由计时器控制,此处不加以讨论。下面,我们来看看 UI 线程的创建过程以及它与 MDI 的结合。

  通过菜单命令 New Bounce ,可在主框架窗口类中响应菜单命令,函数代码如下:

   void CMainFrame::OnBounce()

   {

   CBounceMDIChildWnd *pBounceMDIChildWnd = new CBounceMDIChildWnd;

   if (!pBounceMDIChildWnd->Create( _T("Bounce"),

   WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, rectDefault, this))

   return;

   }

  函数调用子框架窗口的创建函数,代码如下:

   BOOL CBounceMDIChildWnd::Create(LPCTSTR szTitle, LONG style, const RECT& rect, CMDIFrameWnd* parent)

   {

   // Setup the shared menu

   if (menu.m_hMenu == NULL)

   menu.LoadMenu(IDR_BOUNCE);

   m_hMenuShared = menu.m_hMenu;

   if (!CMDIChildWnd::Create(NULL, szTitle, style, rect, parent))

   return FALSE;

   CBounceThread* pBounceThread = new CBounceThread(m_hWnd);

   pBounceThread->CreateThread();

   return TRUE;

   }

  当 CBounceMDIChildWnd 子窗口被删除时, Windows 会同时删除 CBounceWnd 窗口 ( 内嵌在线程对象 pBounceThread ) ,因为它是 CBounceMDIChildWnd 的子窗口。由于 CBounceWnd 运行在单独的线程中,故当 CBounceWnd 子窗口被删除时, CWinThread 线程对象也会自动被删除。

  上述函数生成一个新的 UI 线程对象 pBounceThread ,并调用 CreateThread 函数创建线程。至此,线程已被创建,但还需要做初始化工作,如下面的函数 InitInstance 所示:

   int CBounceThread::InitInstance()

   {

   CWnd* pParent = CWnd::FromHandle(m_hwndParent);

   CRect rect;

   pParent->GetClientRect(&rect);

   BOOL bReturn = m_wndBounce.Create(_T("BounceMTChildWnd"),WS_CHILD | WS_VISIBLE, rect, pParent);

   if (bReturn)

   m_pMainWnd = &m_wndBounce;

   return bReturn;

   }

  注意:这里,将 m_pMainWnd 设置为新创建的 CBounceWnd 窗口是必需的。只有这样设置了,才能保证当 CBounceWnd 窗口被删除时,线程会被自动删除。

  ( 3 Mutexes

  例程 Mutexes 是一个对话框程序。除主线程外,还有两个线程:一个用于计数,一个用于显示。在本例中,这两个线程都是从 CWinThread 派生出来的,但并不用于消息循环处理,派生类重载了 Run 函数,用于完成其计数和显示的任务。

  在对话框类中使用了一个内嵌的 CMutex 对象。对话框初始化时创建两个线程,并设置相应的参数,然后启动运行两个线程。

  当用户设置了对话框的同步检查框标记后,两个线程的同步处理有效。在计数线程的循环中,先调用 CSingleLock::Lock 函数,然后进行计数修改,最后调用 CSingleLock::Unlock 函数。注意:这里的 CSingleLock 对象根据主对话框的 CMutex 对象产生。在显示线程的循环中,先调用 CSingleLock::Lock 函数,然后取到计数值,最后调用 CSingleLock::Unlock 函数。注意:这里的 CSingleLock 对象也是由主对话框的 CMutex 对象产生。类似这种情况 : 一个线程要读取数据,另一个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过联机帮助来获取。

  五、结束语

  多线程函数是 Win32 不同于 Win16 的一个重要方面,其编程技术较为新颖,在程序设计思路上不同于传统的模块结构化方法,比一般的面向对象的思路也较为复杂,尤其是对于多处理器平台的处理更为复杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或不能同步处理的现象。

  在其它的开发语言(如 Visual Basic 5.0 )中也提供了对多线程的支持,但从性能和安全的角度考虑,这种多线程支持受到较多的限制。不过,就一般应用而言,用这种处理方法已经足够了。

  目前,大多数的计算机都是单处理器 (CPU) 的,在这种机器上运行多线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为了抢夺对 CPU 的控制权,则在线程切换时会消耗很多的 CPU 资源,但对于大部分时间被阻塞的线程(例如等待文件 I/O 操作),则可用一个单独的线程来完成。这样,就可将 CPU 时间让出来,使程序获得更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视具体情况加以处理,使应用程序获得最佳的性能。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值