望麓自卑—湖南大学最具潜力的校园传媒

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 1692|回复: 7

[电气与信息工程学院] Visual C++ 5.0中的多线程编程技术

[复制链接]
发表于 2004-9-21 13:21:51 | 显示全部楼层 |阅读模式
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
注意:类的DECLARE_DYNCREATE 和
IMPLEMENT_DYNCREATE宏是必需的,因为创建线程时需要动态
创建类的对象。根据需要可将初始化和结束代码分别放到类的
InitInstance和ExitInstance函数中。如果需要创建窗口,则可在
InitInstance函数中完成。
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++提供的调试工具,以保证共享资源的安全存取。线程安全编程的
另一缺点是运行效率相对要低些,即使在单个线程运行的情况下也会
损失一些效率。所以,我们在实际工作中应具体问题具体分析,以选
择合适的编程方法。

四. 多线程编程例程分析

上面讲述了在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.
   :ostMessage(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_hEventKillRecal
cThread, 0)
          == WAIT_OBJECT_0)
        {
            if (hwndNotifyProgress != NULL)
            {
               pWndNotifyProgress->ostMessage(WM_USER_RECALC_IN_PROG
RESS);
            }
            return FALSE; // Terminate this recalculation
        }
        if (pInfo != NULL&&WaitForSingleObject(pInfo->m_hEventStartRecal
c, 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:ock函数,然后进行
计数修改,最后调用CSingleLock::Unlock函数。注意:这里的
CSingleLock对象根据主对话框的CMutex对象产生。在显示线程的循
环中,先调用CSingleLock:ock函数,然后取到计数值,最后调用
CSingleLock::Unlock函数。注意:这里的CSingleLock对象也是由主对
话框的CMutex对象产生。类似这种情况:一个线程要读取数据,另一
个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情
况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过
联机帮助来获取。

五、结束语

多线程函数是Win32不同于Win16的一个重要方面,其编程技术较为
新颖,在程序设计思路上不同于传统的模块结构化方法,比一般的面
向对象的思路也较为复杂,尤其是对于多处理器平台的处理更为复
杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过
程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程
部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能
地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或
不能同步处理的现象。
在其它的开发语言(如Visual Basic 5.0)中也提供了对多线程的支持,
但从性能和安全的角度考虑,这种多线程支持受到较多的限制。不过,
就一般应用而言,用这种处理方法已经足够了。
目前,大多数的计算机都是单处理器(CPU)的,在这种机器上运行多
线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为
了抢夺对CPU的控制权,则在线程切换时会消耗很多的CPU资源,
但对于大部分时间被阻塞的线程(例如等待文件I/O操作),则可用
一个单独的线程来完成。这样,就可将CPU时间让出来,使程序获得
更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视
具体情况加以处理,使应用程序获得最佳的性能。
 楼主| 发表于 2004-9-21 13:21:51 | 显示全部楼层 |阅读模式
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
注意:类的DECLARE_DYNCREATE 和
IMPLEMENT_DYNCREATE宏是必需的,因为创建线程时需要动态
创建类的对象。根据需要可将初始化和结束代码分别放到类的
InitInstance和ExitInstance函数中。如果需要创建窗口,则可在
InitInstance函数中完成。
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++提供的调试工具,以保证共享资源的安全存取。线程安全编程的
另一缺点是运行效率相对要低些,即使在单个线程运行的情况下也会
损失一些效率。所以,我们在实际工作中应具体问题具体分析,以选
择合适的编程方法。

四. 多线程编程例程分析

上面讲述了在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.
   :ostMessage(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_hEventKillRecal
cThread, 0)
          == WAIT_OBJECT_0)
        {
            if (hwndNotifyProgress != NULL)
            {
               pWndNotifyProgress->ostMessage(WM_USER_RECALC_IN_PROG
RESS);
            }
            return FALSE; // Terminate this recalculation
        }
        if (pInfo != NULL&&WaitForSingleObject(pInfo->m_hEventStartRecal
c, 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:ock函数,然后进行
计数修改,最后调用CSingleLock::Unlock函数。注意:这里的
CSingleLock对象根据主对话框的CMutex对象产生。在显示线程的循
环中,先调用CSingleLock:ock函数,然后取到计数值,最后调用
CSingleLock::Unlock函数。注意:这里的CSingleLock对象也是由主对
话框的CMutex对象产生。类似这种情况:一个线程要读取数据,另一
个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情
况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过
联机帮助来获取。

五、结束语

多线程函数是Win32不同于Win16的一个重要方面,其编程技术较为
新颖,在程序设计思路上不同于传统的模块结构化方法,比一般的面
向对象的思路也较为复杂,尤其是对于多处理器平台的处理更为复
杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过
程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程
部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能
地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或
不能同步处理的现象。
在其它的开发语言(如Visual Basic 5.0)中也提供了对多线程的支持,
但从性能和安全的角度考虑,这种多线程支持受到较多的限制。不过,
就一般应用而言,用这种处理方法已经足够了。
目前,大多数的计算机都是单处理器(CPU)的,在这种机器上运行多
线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为
了抢夺对CPU的控制权,则在线程切换时会消耗很多的CPU资源,
但对于大部分时间被阻塞的线程(例如等待文件I/O操作),则可用
一个单独的线程来完成。这样,就可将CPU时间让出来,使程序获得
更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视
具体情况加以处理,使应用程序获得最佳的性能。
发表于 2004-9-21 20:16:43 | 显示全部楼层
有没有VB.NET有关的编程语言方面的资料~
求助VB.NET高手来指导..!~要是全方面的指导.
发表于 2004-9-21 20:34:00 | 显示全部楼层
我是菜鸟,需要手把手教
 楼主| 发表于 2004-10-2 11:09:26 | 显示全部楼层
惭愧,.net没有用过,帮不了你了,你可以去水母的DotNet版转转,那儿有很多高手
发表于 2004-10-4 21:17:45 | 显示全部楼层
啊,一只小鸟掉下水,掉下水,掉下水。。。。。
发表于 2004-11-18 15:03:08 | 显示全部楼层
菜鸟学vc,根本就不懂,希望有人能交流交流
有没有人用《Visual C++6.0 MFC时尚编程百例 》一书的人?
希望可以交流。
第一个实例我就不能运行,错误为无法找到“LookMaIDS.psh”不可达或文件不存在
请指点。
发表于 2004-11-18 15:03:41 | 显示全部楼层
菜鸟学vc,根本就不懂,希望有人能交流交流
有没有人用《Visual C++6.0 MFC时尚编程百例 》一书的人?
希望可以交流。
第一个实例我就不能运行,错误为无法找到“LookMaIDS.psh”不可达或文件不存在
请指点。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

每日推荐上一条 /1 下一条

小黑屋|手机版|湖南大学望麓自卑校园传媒 ( 湘ICP备14014987号 )

GMT+8, 2024-11-25 01:32 , Processed in 0.234391 second(s), 23 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表