1.67k likes | 1.81k Views
第十一章 多任务与多线程. 所谓多任务就是指在一个计算机系统平台上一个以上的 任务 被 同时 执行。在 单 CPU 的单一计算机平台上,多任务是由单个 CPU 分时 完成的;在 多 CPU 的单一计算机上,多任务是由多个 CPU 协同并行 (包括部分分时)完成的;在 分布式系统 中, 多 任务 是分别由系统中的 多台计算机独立 或 协同并行 完成的。 多 任务 的表现形式可以分为两种: ⑴ 系统平台上的多任务 :同时运行的 多个进程 。这些进程之间 可以相关或互不相关。 ⑵ 进程中的多任务 :在同一进程中同时运行的 多个线程 ,一般
E N D
所谓多任务就是指在一个计算机系统平台上一个以上的任务所谓多任务就是指在一个计算机系统平台上一个以上的任务 被同时执行。在单 CPU的单一计算机平台上,多任务是由单个 CPU 分时完成的;在多 CPU 的单一计算机上,多任务是由多个 CPU 协同并行(包括部分分时)完成的;在分布式系统中,多 任务是分别由系统中的多台计算机独立或协同并行完成的。多 任务的表现形式可以分为两种: ⑴ 系统平台上的多任务:同时运行的多个进程。这些进程之间 可以相关或互不相关。 ⑵ 进程中的多任务:在同一进程中同时运行的多个线程,一般 情况下,这些线程必定密切相关,并且协同完成进程的共同 目标,使进程的运行更加高效。
Win32 API 的抢先式多任务和多线程编程机制为实现上述多任务提供了最好的支持。这种支持主要表现在: ·Windows消息处理 ·空闲状态处理 ·多线程编程
11.1 Windows消息处理 为了理解线程,你首先必须理解 32 位 Windows 系统是如何 处理消息的。从一个单一线程程序开始,看看它的消息翻译和 分发过程。 11.1.1线程程序处理消息的过程 深入到 MFC 的源代码(它们被连接到你的程序中)可以看到消息翻译和分发过程的机制也可以通过下列语句描述: MSG massage; while(::GetMessage(&message, NULL, 0, 0)) { ::TranslateMessage(&message); ::DispatchMessage(&message); }
API函数 GetMessage 是一个阻塞型函数,即只有在收到一个 消息时,函数调用才返回;否则函数调用被阻塞。Windows系 统根据消息 massage ,决定消息属于哪个进程,如果没有属于 某个进程的消息发出,则该进程就被 “挂起”,而其他进程仍可 以按原来的状态运行;当该进程的一个消息到达时,该进程被 “唤醒” ,并执行如下操作过程: ·调用 API函数 TranslateMessage翻译消息,如将 WM_KEYDOWN 消息翻译成包含相应按键的 ASCII 码的 WM_CHAR 消息; ·调用 API函数 DispatchMessage通过窗口类把控制交给 MFC 消 息映射机构处理收到的消息;
·MFC 消息映射机构通过消息映射调用相应的处理函数; ·消息控制函数执行相应的功能操作; ·控制权返回到 MFC 应用程序框架, ·DispatchMessage的调用返回。
11.1.2 交出控制 由于 DispatchMessage 要等到消息响应函数返回后才能返回, 那么,当某个消息响应函数的执行过于耗时,在该函数执行期 间只有鼠标还可以移动,有些其它基于系统中断的任务仍可执 行外,不能处理任何其他消息。在 Win32 模式下,虽然抢先式 多任务机制,会使上述情况的发生大大减少,但并不能避免。 然而,对这个问题有一个缓解的办法,你只需修改该耗时的 函数,使它隔一段时间就交出控制一次。例如,你可以在该函 数的主循环中插入下面的语句:
MSG massage; while(::PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) { ::TranslateMessage(&message); ::DispatchMessage(&message); }
函数 PeekMessage 的功能与 GetMessage 类似,都用于消息的获 取,而不同之处在于, PeekMessage 的执行是非阻塞型的,也 就是说,在没有消息到达进程(消息队列中没有该进程要处理 的消息)时,PeekMessage 的执行也会马上返回,从而使调用 PeekMessage 的消息响应函数能继续执行;如果有一个消息到 达进程的话,该消息响应函数将被暂停,而使 PeekMessage 获 取的消息通过调用 API函数 DispatchMessage 得到相应的响应, 待消息响应返回后,该消息响应函数继续执行。 显然,使用上述方法的关键是插入暂时交出控制操作的频度 问题。频度过高,将严重影响响应函数的执行效率;频度过 低,将不能有效地解决耗时响应函数执行期间的其他消息的响 应问题。
11.2 定时器 Windows 的定时器是一个非常有用的编程元素,在有些情况 下,它会使多线程编程变得似乎没有必要。例如,你需要顺序 读取一个通讯缓冲区中每100 毫秒所积累的数据,则可以设置一 个100 毫秒间隔的定时器来实现;又如,你还可以用定时器控制 动画频率等。定时器的使用非常简单,典型的操作步骤如下: 1 使用 ClassWizard 为 WM_TIMER消息建立映射(消息映射、消 息控制函数的声明和定义)用于响应定时器中断消息。 2 用指定的时间间隔参数调用 CWnd::SetTimer函数,启动定时 器工作。 3 当不需要再次使用定时器时,则调用 CWnd::KillTimer,停止定 时器工作。
注意: ·由于 Windows 不是实时系统,如果你定义的定时器时间间隔 少于 100 毫秒,定时器可能不会很精确。 ·与其它 Windows 消息一样 WM_TIMER消息也可能被其它控制 函数所阻塞。所幸的是,定时器消息不会被累加起来,即如 果队列中已经有了定时器消息的话,Windows系统不会把同 样的定时器消息再放入消息队列中。 ·定时器是系统资源,因此应该注意及时释放已经不使用的定 时器;同时,过多过滥地使用定时器资源会影响程序的执行 效率和运行状态。 返回
11.3 空闲状态处理 在引入多线程编程之前,Windows 程序员常常使用空闲状态 处理来作一些后台任务。如今,空闲状态处理已经不再那麽重 要了,但对于处理有些必要的后台任务时仍然非常有效,故此 被保留了下来。在程序设计中,你可以通过重载 CWinApp::OnIdle 或 CWinThread::OnIdle 来实现处理应用程序框架或某个线程中的 后台工作。OnIdle 是在消息循环里被调用的,所以实际的消息循 环比典型的消息循环描述代码更复杂。OnIdle 的原型如下: virtual BOOL OnIdle( LONG lCount ); 该函数在消息循环中被调用的情况: 1 当消息队列中没有待处理的消息时,OnIdle 被调用;
2 基类中的 OnIdle 将对工具栏、状态栏等框架界面进行更新; 3 基类中的 OnIdle 返回false,这意味着此次 OnIdle 被调用后, 如果消息队列中继续没有待处理消息的状态,则 OnIdle 将不 能被再次调用;直到消息循环获得了新消息并处理后,消息 队列再一次出现空闲时,OnIdle 才会被再次调用。如果 OnIdle 返回true,则只要消息队列中无新消息到达, OnIdle 就会被 再次调用。 4 参数 lCount 表示相邻两次消息处理之间 OnIdle 被连续调用次 数(每次 OnIdle 被调用 lCount增1,每次消息处理将 lCount 置 0),换句话说,lCount 可以指示进程或线程已经空闲了多长 时间。因此,你在重载 OnIdle 函数中可以根据框架传来的 lCount确定不同的操作。
5 如果用户运行一个模式对话框或者正在使用菜单,则这时 OnIdle 不会被响应。如果你希望在模式对话框或者正在使用 菜单的情况下使用后台处理,则你必须加入 WM_ENTERIDLE 的映射(消息控制函数为 OnEnterIdle)。注意,此消息映射 一般添加在框架窗口类中而不是视图类中,因为弹出式对话 框和菜单总是属于应用程序的框架窗口。
在重载 OnIdle 函数应注意以下几点: 1 由于在 OnIdle 函数执行期间消息循环不能响应消息,因此, 在 OnIdle 中不要安排耗时的操作,否则会将低进程的响应速 度和运行效率。 2 在 OnIdle 重载代码中应包含对基类 OnIdle 的调用,否则将会 失去对工具栏、状态栏等框架界面进行更新的功能。一般的 重载代码应安排如下: CMyApp::OnIdle( LONG lCount ) { // 后台处理代码 return CWinApp::OnIdle( lCount ); }
3 如果你希望在两次消息响应之间的空闲阶段中 OnIdle 被连续 调用,则应将重载代码安排修改如下: CMyApp::OnIdle( LONG lCount ) { CWinApp::OnIdle( lCount ); // 后台处理代码 return TRUE; } 返回
11.4 多线程编程 从前面的描述中不难看出,通过 Windows 消息处理机制和空 闲状态处理实现多任务对于程序设计来说,限制较多、主动灵 活性较差。如果你希望在一个程序中安排多条相互独立、可以 同时运行(并行)的执行路径,来协同完成程序的运行目标, 例如,对一个图象序列的实时采集、序列图象的转储和部分图 象处理三条执行路径同时执行,以满足对动态图象进行实时采 集、转储和处理的要求。对于这样的功能需求,显然前两种实 现多任务的机制是不可能满足的,而在同一程序中采用多线程 编程的编程方法将能很好地满足这一需求。
一个的进程可以同时有多条执行路径,每条执行路径称为线一个的进程可以同时有多条执行路径,每条执行路径称为线 程。大多数情况下,进程中的所有代码和数据空间可以被进程 内的所有线程所使用。线程的状态和执行由操作系统管理和控 制,且每个线程可以具有自己的堆栈。因此,一个线程就像是 进程内部又运行了另一个 “子进程” ,它既不必像消息机制那样 抢先运行,也不必像后台任务那样等到进程空闲时才能执行它 的任务。而且一个线程的运行也不会导致进程中的其它线程 (包括进程的主线程)“停止”,直到该线程终止或被中断。
Windows 提供了两种线程,辅助线程(操作人员线程)和用 户界面线程。 ·用户界面线程运行的是由一个 CWinThread 或 CWinThread 的自 定义派生类的对象实例实现的,用户界面线程可以有自己的 消息循环和响应处理消息的能力,也可以根据需要拥有自己 的操作界面。应用程序类的基类 CWinApp是 CWinThread 的特 定派生类(能处理整个进程的进入、运行、退出),因此, 应用程序可以视为一个特殊的线程—— 主线程; ·辅助线程运行的是由一个控制函数(既可以是全程函数,也 可以是某个类的成员函数)的执行实现的,因此,辅助线程 没有消息循环和操作界面。
11.4.1 辅助线程函数、用户界面线程类和线程启动 辅助线程是通过系统启动一个由程序设计者编写的全程函数 或某个类的成员函数实现的。该函数包括了完成相应线程任务 的全部代码,它的返回应该是 UINT类型值,并且使用一个 32 位的 LPVOID 类型指针参数传递线程运行中所需要的数据。此 类线程使用较多。例如,一个辅助线程函数可以如下设计: UINT ComputeThreadProc( LPVOID pParam ) { // Do thread processing … return 0; } 然后调用 AfxBeginThread 函数启动线程。
用户界面线程是通过系统创建并启动一个由程序设计者声明用户界面线程是通过系统创建并启动一个由程序设计者声明 并定义的 CWinThread派生类实现的。用 RUNTIME_CLASS 宏获 得该类的运行时类结构用于运行时创建类对象。显然,用户界 面线程类的定义与建立一个用户程序类类似,一般的创建步骤 如下所示: ·从 CWinThread派生定义一个线程类; ·如果该线程类的对象需要静态创建,则构造函数的 protected 访问属性需要改为 public; ·为该线程类添加线程运行管理所需要的属性和方法; ·重定义该线程类的虚函数Run并在 Run函数中写一个无限循 环结构,线程要执行的操作均安排在这个无限循环结构中;
·如果需要,可以使用 Sleep 函数控制无限循环的执行速度; ·如果需要,可以为线程创建主窗口 m_pMainwnd 并添加界面; ·将线程类的定义头文件包含到需要创建线程的 .cpp 文件中; ·定义一个该线程类指针; ·调用 AfxBeginThread 函数启动线程,如果需要在一段时间内 连续使用一个线程实例,也可以用线程类的 CreateThread 成 员函数启动线程。 创建和启动两种线程都可以调用 MFC 全局函数 AfxBeginThread 完成。该函数被调用时,第一个调用实参是辅助线程函数或用 户界面线程类。下面给出了 AfxBeginThread的两种原型,分别用 于辅助线程和用户界面线程:
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 );
其中: 1 第一函数原型的第二参数 pParam用于为辅助线程传递数据。 2 两种函数原型的后四个参数是一致的,它们的含义分别为: ⑴ nPriority为线程优先级,参见下表:
Windows 系统会在执行的线程之间,根据它们的优先级来分 配线程的占用时间。 ⑵nStackSize为线程所使用的堆栈大小,nStackSize= 0表示线 程所使用的堆栈大小与创建该线程的线程相同。 ⑶dwCreateFlags为线程的创建标志: ⑷lpSecurityAttrs指向安全属性结构,如果 lpSecurityAttrs = 0, 表示该线程的安全属性与创建它的线程相同。注意,除了创 建 Windows NT 平台上的程序以外,该参数应选择缺省值 0。
11.4.2 多线程对共享资源的访问 如果同时运行的多个线程都只访问属于各个线程自己的资源 (数据),那麽线程对数据的访问无疑是安全的。但往往设计 多线程程序的目的就是要对一些资源(数据)同时进行各种共 享操作,例如,前面提及的实时图象采集、图象数据转储和特 定的图象处理三个线程对共同的图象数据的访问。此外,主线 程与辅助线程之间以及辅助线程之间的通讯也需要通过访问共 享资源来完成的。多线程对共享数据的访问将引起冲突问题。 例如,三个线程共享一个计数器,众所周知,计数器中的计数 是不可重复的。如果三个线程只读取计数器中计数而不修改, 显然不存在问题。而如果三个线程对计数器的访问会引起计数 变化时,则计数器就可能产生重复的计数:
线程1检查计数器值 处理器控制权转移 计数器=1 线程2检查计数器值 计数器=1 线程2将计数器值增1 处理时间 计数器=2 处理器控制权转移 线程3检查计数器值 计数器=2 线程3将计数器值增1 计数器=3 处理器控制权转移 线程1将计数器值 (认为仍为1)增1 计数器=2 计数值 1 2 3 2
显然,这样的共享在多线程的应用程序中不能很好地工作,显然,这样的共享在多线程的应用程序中不能很好地工作, 这是因为对一个共享资源不加任何制约的访问会引起在一个线 程访问资源的过程中,资源已经被其它线程修改,使得它对资 源的操作依据发生了错误。为了避免这样的错误发生,MFC提 供了四种同步机制用于限制共享资源的访问操作,以便确保多 个同时运行线程之间在访问共享资源时的处理操作被同步。这 四种同步机制以不同的方式工作,采用那一种机制取决于不同 的线程运行环境需要。这四种同步机制是:临界区、互斥、信号量和事件。
继续对资源的访问 临界区解锁,退出临界区 11.4.2.1 临界区 临界区是一种把对某个资源的访问权限制在同进程的单个线 程的机制。这种机制使得一个线程在访问共享资源之前必须进 入临界区,并在完成对该资源的访问后退出临界区。如果共享 资源正被另一个线程访问,则该线程将被阻塞,无法进入临界 区,直到正在访问共享资源的线程退出临界区,该线程才能进 入临界区访问共享资源。该同步机制的工作可以描述为: if (无线程正在访问,即临界区未加锁) 进入临界区,临界区加锁。 else 等待临界区解锁,线程被阻塞
CSyncObject CCriticalSection MFC 为临界区同步机制提供了 CCriticalSection 类: CCriticalSection 类提供了 “加锁” 和 “解锁” 两种方法: BOOL Lock(); BOOL Lock( DWORD dwTimeout ); 返回值:如果成功返回非0,否则返回0 参数: dwTimeoutLock 忽略此参数值。 CObject
说明:调用该成员函数用于获得对临界区对象的访问。Lock 是 一个阻塞调用,也就是说,该函数在临界区没有被信号触发 处于解锁状态之前,将不会返回。如果需要定时等待,你可 以使用互斥类 CMutex代替 CCriticalSection。 virtual BOOL Unlock(); 返回值:如果 CCriticalSection 对象被某个线程拥有,并且释放 成功,则返回非0,否则返回0。 说明:释放临界区 CCriticalSection 对象,以便其它线程使用。 如果 CCriticalSection 正在被某个线程独自占用,那麽在该临 界区控制的资源使用结束后,必须立即调用 Unlock。
一旦创建了 CCriticalSection 对象,就可以在线程访问受控资源时,作为参数创建 CSingleLock 对象,例如: CCriticalSection m_criticalSection; CSingleLock m_singleLock( &m_criticalSection ); 或使用该同步对象的数组(数组中包含一个以上 CCriticalSection 对象)作为参数创建 CMultiLock 对象。例如: CCriticalSection m_criticalSections[10]; CMultiLock m_multiLock( &m_criticalSections ); 然后使用 CSingleLock 或者 CMultiLock 的成员函数 Lock 和 Unlock 对所访问的受控资源加锁和解锁。
CSingleLock 和 CMultiLock 类描述了多线程应用程序的访问控 制机制。由于 CSingleLock 类的构造函数使用 CSyncObject类型指 针作为参数,因此,如果线程访问共有资源时只需要等待一个 同步对象来控制访问,则使用 CSingleLock 对象实现访问控制; CMultiLock 类的构造函数使用 CSyncObject类型指针数组作为参 数,因此,如果线程访问共有资源时需要等待多个同步对象来 控制访问,则使用 CMultiLock 对象实现访问控制。CMultiLock 特 别多用于由多个 CEvent同步类对象控制的多线程应用程序。
线程1进入临界区 线程1检查计数器值 处理器控制权转移 计数器=1 处理器控制权转移 线程1将计数器增1 处理器控制权转移 处理时间 计数器=2 处理器控制权转移 线程3进入临界区 线程3检查计数器值 计数器=2 线程3将计数器增1 计数器=3 线程2进入临界区 线程2检查计数器值 处理器控制权转移 计数器=3 线程2将计数器增1 计数器=4 如果将前面所描述的三个线程访问的计数器的共享操作处于 临界区同步机制的控制之下,则操作过程可以描述如下: 计数值 1 线程2试图进入 临界区,但被阻塞 线程3试图进入 临界区,但被阻塞 2 线程1离开临界区 3 线程3离开临界区 4 线程2离开临界区
示例程序1 该示例程序 “Mthread”的目的是描述在应用程序的主线程和 辅助线程之间如何安全共享简单数据变量(系统预定义类型数 据变量)。在该示例程序中: ·主线程:响应定时器以一个确定的间隔发出的通知消息, 并 在响应函数中读取一个由辅助线程增值的共享计数器值,并 依据此值来确定一个进度条控件递增显示位置。 ·辅助线程:以特定的速度逐步地递增共享计数器的值,并在 达到某一特定值时结束辅助线程的运行。
1 创建一个项目名为 “Mthread”的 SDI 应用程序。 2 在应用程序中增加一个对话框资源 IDD_COMPUTE 和相应的类 CComputeDlg。该对话框中包含以下控件:
3 在 CComputeDlg 类中增加如下消息响应函数: void CComputeDlg::OnStart() { m_nTimer = SetTimer( 1, 100, NULL ); // 启动100 ms 间隔的定时器 ASSERT( m_nTimer != 0 ); GetDlgItem( IDC_START )->EnableWindow( FALSE ); AfxBeginThread( ComputeThreadProc, GetSafeHwnd(), THREAD_PRIORITY_NORMAL ); // 启动辅助线程 }
其中调用 AfxBeginThread 创建辅助线程时的第一个参数是线 程函数 ComputeThreadProc ,而第二个参数为该对话框的窗 口句柄,用于辅助线程结束时向主线程发出结束消息。 void CComputeDlg::OnCancel() { if (g_nCount == 0) { // prior to Start button CDialog::OnCancel(); } else { // computation in progress g_nCount = nMaxCount; // Force thread to exit } }
其中 g_nCount 为主线程与辅助线程共享的计数器变量,它是 一个全局变量。nMaxCount 为设定的计数限定值。 使 g_nCount = nMaxCount,意味着强行结束正在运行的辅助 线程。 void CComputeDlg::OnTimer( UINT nIDEvent ) { CProgressCtrl* pBar = (CProgressCtrl*) GetDlgItem(IDC_PROGRESS1); pBar->SetPos( g_nCount * 100 / nMaxCount ); }
显然,调用进度条的成员函数 SetPos 确定进度条的渐进显示 位置是由共享计数器变量决定的。 LRESULT CComputeDlg::OnThreadFinished( WPARAM wParam, LPARAM lParam ) { KillTimer(m_nTimer); CDialog::OnOK(); return 0; } 显然,辅助线程结束时,将通过发送用户消息使对话框结束 运行。
4 辅助线程的执行函数是一个全局函数,其代码如下: UINT ComputeThreadProc( LPVOID pParam ) { volatile int nTemp; // volatile else compiler optimizes too much for (g_nCount = 0; g_nCount < CComputeDlg::nMaxCount; ::InterlockedIncrement( (long*) &g_nCount) ) { for (nTemp = 0; nTemp < 10000; nTemp++) { // uses up CPU cycles } } // WM_THREADFINISHED is user-defined message ::PostMessage((HWND) pParam, WM_THREADFINISHED, 0, 0); g_nCount = 0; return 0; // ends the thread }
几点说明: ⑴ 使用 volatile 声明,会使变量 nTemp 不优化,即不会在循环 中被保存在一个寄存器中,避免在多进程应用程序中引起的 循环不终止。 ⑵ 使用 Win32 函数 InterlockedIncrement 完成共享计数器变量 g_nCount 的加锁并增1操作,可以防止多个线程对该变量的 同时修改。 ⑶ 辅助线程结束时向主线程发出一个用户消息通告辅助线程退 出。该用户消息定义为: #define WM_THREADFINISHED WM_USER + 5
5 在视图类 CMThreadView中添加鼠标左键按下的响应,用于 CComputeDlg 类对象的模态调用。 void CMThreadView::OnLButtonDown( UINT nFlags, CPoint point ) { CComputeDlg dlg; dlg.DoModal(); } 6 编译执行 “Mthread”
示例程序2 在前面示例程序中,对共享计数器的修改操作使用了 Win32 函数 InterlockedIncrement,但这种防止多个线程同时修改共享 的变量只限于简单变量的增1操作,而对于复杂共享数据结构 的复杂操作则必须使用临界区或其它同步机制,以便保证多线 程共享数据的安全。 本示例 “Mthread1” 是在前面示例的基础上,将主线程与辅助 线程共享的简单计数器变为较为复杂的 “时分秒”计数器,并通 过辅助线程对该计数器进行修改,从而控制主线程的进度显示 操作。为实现此目的,需要对前示例程序做如下修改和添加: 1 增加一个“时分秒”计数器类 CHMS定义:
class CHMS { private: int m_nHr, m_nMn, m_nSc; CCriticalSection m_cs; public: CHMS() : m_nHr( 0 ), m_nMn( 0 ), m_nSc( 0 ) { } ~CHMS() {} void SetTime( int nSecs ) { m_cs.Lock(); m_nSc = nSecs % 60;
m_nMn = (nSecs / 60) % 60; m_nHr = nSecs / 3600; m_cs.Unlock(); } int GetTotalSecs() { int nTotalSecs; m_cs.Lock(); nTotalSecs = m_nHr * 3600 + m_nMn * 60 + m_nSc; m_cs.Unlock(); return nTotalSecs; }
void IncrementSecs() { SetTime( GetTotalSecs() + 1 ); } CStringGetHr_Mn_Sc() { CString str; m_cs.Lock(); str.Format( "%2d:%2d:%2d",m_nHr, m_nMn, m_nSc ); m_cs.Unlock(); return str; } };
几点说明: ⑴ 将共享数据和对该数据的操作封装在一个类 CHMS 中; ⑵ 将临界区 CCriticalSection 类对象 m_cs作为 CHMS 的成员; ⑶ 在相应的数据操作成员函数中加入: ·对数据实行操作前进入临界区,防止多个线程同时修改; ·完成对数据的操作后退出临界区,以便其它线程使用。 例如:SetTime、GetTotalSecs 和IncrementSecs
2 将共享计数器 g_nCount 的类型变为 CHMS,为此 CComputeDlg 各个成员函数中对 g_nCount 的操作必须做如下修改: void CComputeDlg::OnCancel() { if (g_nCount.GetTotalSecs() == 0) // prior to Start button CDialog::OnCancel(); else // Force thread to exit g_nCount.SetTime( nMaxCount ); } void CComputeDlg::OnTimer( UINT nIDEvent ) { CProgressCtrl* pBar = (CProgressCtrl*) GetDlgItem(IDC_PROGRESS1); pBar->SetPos( g_nCount.GetTotalSecs() * 100 / nMaxCount ); }
3 辅助线程函数也需要做如下修改: UINT ComputeThreadProc( LPVOID pParam ) { volatile int nTemp; // volatile else compiler optimizes too much for (g_nCount.SetTime(0); g_nCount.GetTotalSecs() < CComputeDlg::nMaxCount; g_nCount.IncrementSecs()) { for (nTemp = 0; nTemp < 100000; nTemp++) { // uses up CPU cycles } SetDlgItemText((HWND) pParam, IDC_TIME, g_nCount.GetHr_Mn_Sc()); }