计算机世界1998年.第50期
用VC++5.0实现多线程的调度和处理
哈尔滨工程大学一系研究生
张万春
一、多任务,多进程和多线程
Windows95 和WindowsNT
操作系统支持多任务调度和处理,基于该功能所提供的多任务空间,程序员可以完全控制应用程序中每一个片段的运行,从而编写高效率的应用程序。
所谓多任务通常包括这样两大类:多进程和多线程。进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务。
开发多线程应用程序可以利用32 位Windows 环境提供的Win32 API
接口函数,也可以利用VC++ 中提供的MFC
类库进行开发。多线程编程在这两种方式下原理是一样的,用户可以根据需要选择相应的工具。本文重点讲述用VC++5.0
提供的MFC
类库实现多线程调度与处理的方法以及由线程多任务所引发的同步多任务特征,最后详细解释一个实现多线程的例程。
二、基于MFC 的多线程编程
1 MFC 对多线程的支持
MFC
类库提供了多线程编程支持,对于用户编程实现来说更加方便。非常重要的一点就是,在多窗口线程情况下,MFC
直接提供了用户接口线程的设计。
MFC 区分两种类型的线程:辅助线程(Worker Thread)和用户界面线程(UserInterface
Thread)。辅助线程没有消息机制,通常用来执行后台计算和维护任务。MFC
为用户界面线程提供消息机制,用来处理用户的输入,响应用户产生的事件和消息。但对于Win32
的API
来说,这两种线程并没有区别,它只需要线程的启动地址以便启动线程执行任务。用户界面线程的一个典型应用就是类CWinApp,大家对类CwinApp
都比较熟悉,它是CWinThread
类的派生类,应用程序的主线程是由它提供,并由它负责处理用户产生的事件和消息。类CwinThread
是用户接口线程的基本类。CWinThread
的对象用以维护特定线程的局部数据。因为处理线程局部数据依赖于类CWinThread,所以所有使用MFC
的线程都必须由MFC 来创建。例如,由run-time 函数_beginthreadex
创建的线程就不能使用任何MFC API。
2 辅助线程和用户界面线程的创建和终止
要创建一个线程,需要调用函数AfxBeginThread。该函数通过参数重载具有两种版本,分别对应辅助线程和用户界面线程。无论是辅助线程还是用户界面线程,都需要指定额外的参数以修改优先级,堆栈大小,创建标志和安全特性等。函数AfxBeginThread
返回指向CWinThread 类对象的指针。
创建助手线程相对简单。只需要两步:实现控制函数和启动线程。它并不必须从CWinThread
派生一个类。简要说明如下:
1.
实现控制函数。控制函数定义该线程。当进入该函数,线程启动;退出时,线程终止。该控制函数声明如下:
UINT MyControllingFunction( LPVOID pParam );
该参数是一个单精度32
位值。该参数接收的值将在线程对象创建时传递给构造函数。控制函数将用某种方式解释该值。可以是数量值,或是指向包括多个参数的结构的指针,甚至可以被忽略。如果该参数是指结构,则不仅可以将数据从调用函数传给线程,也可以从线程回传给调用函数。如果使用这样的结构回传数据,当结果准备好的时候,线程要通知调用函数。当函数结束时,应返回一个UINT
类型的值值,指明结束的原因。通常,返回0
表明成功,其它值分别代表不同的错误。
2. 启动线程。由函数AfxBeginThread 创建并初始化一个CWinThread
类的对象,启动并返回该线程的地址。则线程进入运行状态。
3.
举例说明。下面用简单的代码说明怎样定义一个控制函数以及如何在程序的其它部分使用。
UINT MyThreadProc( LPVOID pParam )
{
CMyObject* pObject = (CMyObject*)pParam;
if (pObject == NULL ||
!pObject->IsKindOf(RUNTIME_CLASS(CMyObject)))
return -1; // 非法参数
......// 具体实现内容
return 0; // 线程成功结束
}
// 在程序中调用线程的函数
......
pNewObject = new CMyObject;
AfxBeginThread(MyThreadProc, pNewObject);
......
创建用户界面线程有两种方法。
第一种方法,首先从CWinTread 类派生一个类(注意必须要用宏DECLARE_DYNCREATE
和IMPLEMENT_DYNCREATE 对该类进行声明和实现);然后调用函数AfxBeginThread
创建CWinThread 派生类的对象进行初始化启动线程运行。除了调用函数AfxBeginThread
之外, 也可以采用第二种方法,即先通过构造函数创建类CWinThread
的一个对象,然后由程序员调用函数::CreateThread 来启动线程。通常类CWinThread
的对象在该线程的生存期结束时将自动终止,如果程序员希望自己来控制,则需要将m_bAutoDelete
设为FALSE。这样在线程终止之后类CWinThread
对象仍然存在,只是在这种情况下需要手动删除CWinThread 对象。
通常线程函数结束之后,线程将自行终止。类CwinThread
将为我们完成结束线程的工作。如果在线程的执行过程中程序员希望强行终止线程的话,则需要在线程内部调用AfxEndThread(nExitCode)。其参数为线程结束码。这样将终止线程的运行,并释放线程所占用的资源。如果从另一个线程来终止该线程,则必须在两个线程之间设置通信方法。如果从线程外部来终止线程的话,还可以使用Win32
函数(CWinThread 类不提供该成员函数):BOOL TerminateThread(HANDLE
hThread,DWORD dwExitcode)。但在实际程序设计中对该函数的使用一定要谨慎,因为一旦该命令发出,将立即终止该线程,并不释放线程所占用的资源,这样可能会引起系统不稳定。
如果所终止的线程是进程内的最后一个线程,则在该线程终止之后进程也相应终止。
3 进程和线程的优先级问题
在Windows95 和WindowsNT 操作系统当中,任务是有优先级的,共有32
级,从0 到31,系统按照不同的优先级调度线程的运行。
1) 0-15
级是普通优先级,线程的优先级可以动态变化。高优先级线程优先运行,只有高优先级线程不运行时,才调度低优先级线程运行。优先级相同的线程按照时间片轮流运行。
2) 16-30
级是实时优先级,实时优先级与普通优先级的最大区别在于相同优先级进程的运行不按照时间片轮转,而是先运行的线程就先控制CPU,如果它不主动放弃控制,同级或低优先级的线程就无法运行。
一个线程的优先级首先属于一个类,然后是其在该类中的相对位置。线程优先级的计算可以如下式表示:
线程优先级= 进程类基本优先级+ 线程相对优先级
进程类的基本优先级:
IDLE_PROCESS_CLASS
NORMAL_PROCESS_CLASS
HIGH_PROCESS_CLASS
REAL_TIME_PROCESS_CLASS
线程的相对优先级:
THREAD_PRIORITY_IDLE
(最低优先级,仅在系统空闲时执行)
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_NORMAL (缺省)
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_CRITICAL (非常高的优先级)
4 线程同步问题
编写多线程应用程序的最重要的问题就是线程之间的资源同步访问。因为多个线程在共享资源时如果发生访问冲突通常会产生不正确的结果。例如,一个线程正在更新一个结构的内容的同时另一个线程正试图读取同一个结构。结果,我们将无法得知所读取的数据是什么状态:旧数据,新数据,还是二者的混合?
MFC 提供了一组同步和同步访问类来解决这个问题,包括:
同步对象:CSyncObject, CSemaphore, CMutex, CcriticalSection 和CEvent
;同步访问对象:CMultiLock 和CSingleLock 。
同步类用于当访问资源时保证资源的整体性。其中CsyncObject
是其它四个同步类的基类,不直接使用。信号同步类CSemaphore
通常用于当一个应用程序中同时有多个线程访问一个资源(例如,应用程序允许对同一个Document
有多个View)的情况;事件同步类CEvent
通常用于在应用程序访问资源之前应用程序必须等待(比如,在数据写进一个文件之前数据必须从通信端口得到)的情况;而对于互斥同步类CMutex
和临界区同步类CcriticalSection
都是用于保证一个资源一次只能有一个线程访问,二者的不同之处在于前者允许有多个应用程序使用该资源(例如,该资源在一个DLL
当中)而后者则不允许对同一个资源的访问超出进程的范畴,而且使用临界区的方式效率比较高。
同步访问类用于获得对这些控制资源的访问。CMultiLock 和CSingleLock
的区别仅在于是需要控制访问多个还是单个资源对象。
5 同步类的使用方法
解决同步问题的一个简单的方法就是将同步类融入共享类当中,通常我们把这样的共享类称为线程安全类。下面举例来说明这些同步类的使用方法。比如,一个用以维护一个帐户的连接列表的应用程序。该应用程序允许3
个帐户在不同的窗口中检测,但一次只能更新一个帐户。当一个帐户更新之后,需要将更新的数据通过网络传给一个数据文档。
该例中将使用3 种同步类。由于允许一次检测3 个帐户,使用CSemaphore
来限制对3 个视窗对象的访问。当更新一个帐目时,应用程序使用CCriticalSection
来保证一次只有一个帐目更新。在更新成功之后,发CEvent
信号,该信号释放一个等待接收信号事件的线程。该线程将新数据传给数据文档。
要设计一个线程安全类,首先根据具体情况在类中加入同步类做为数据成员。在例子当中,可以将一个CSemaphore
类的数据成员加入视窗类中,一个CCriticalSection
类数据成员加入连接列表类,而一个CEvent
数据成员加入数据存储类中。
然后,在使用共享资源的函数当中,将同步类与同步访问类的一个锁对象联系起来。即,在访问控制资源的成员函数中应该创建一个CSingleLock
或CMultiLock 的对象并调用该对象的Lock 函数。当访问结束之后,调用UnLock
函数,释放资源。
用这种方式来设计线程安全类比较容易。在保证线程安全的同时,省去了维护同步代码的麻烦,这也正是OOP
的思想。但是使用线程安全类方法编程比不考虑线程安全要复杂,尤其体现在程序调试过程中。而且线程安全编程还会损失一部分效率,比如在单CPU
计算机中多个线程之间的切换会占用一部分资源。
三、编程实例
下面以VC++5.0 中一个简单的基于对话框的MFC
例程来说明实现多线程任务调度与处理的方法,下面加以详细解释。
在该例程当中定义两个用户界面线程,一个显示线程(CDisplayThread)
和一个计数线程(CCounterThread)。这两个线程同时操作一个字符串变量m_strNumber,其中显示线程将该字符串在一个列表框中显示,而计数线程则将该字符串中的整数加1。在例程中,可以分别调整进程、计数线程和显示线程的优先级。例程中的同步机制使用CMutex
和CSingleLock
来保证两个线程不能同时访问该字符串。同步机制执行与否将明显影响程序的执行结果。在该例程中允许将将把两个线程暂时挂起,以查看运行结果。例程中还允许查看计数线程的运行。该例程中所处理的问题也是多线程编程中非常具有典型意义的问题。
在该程序执行时主要有三个用于调整优先级的组合框,三个分别用于选择同步机制、显示计数线程运行和挂起线程的复选框以及一个用于显示运行结果的列表框。
在本程序中使用了两个线程类CCounterThread 和CDisplayThread,这两个线程类共同操作定义在CMutexesDlg
中的字符串对象m_strNumber。本程序对同步类CMutex
的使用方法就是按照本文所讲述的融入的方法来实现的。同步访问类CSingleLock
的锁对象则在各线程的具体实现中定义。
下面介绍该例程的具体实现:
1 利用AppWizard 生成一个名为Mutexes 基于对话框的应用程序框架。
2
利用对话框编辑器在对话框中填加以下内容:三个组合框,三个复选框和一个列表框。三个组合框分别允许改变进程优先级和两个线程优先级,其ID
分别设置为:IDC_PRIORITYCLASS、IDC_DSPYTHRDPRIORITY 和IDC_CNTRTHRDPRIORITY。三个复选框分别对应着同步机制选项、显示计数线程执行选项和暂停选项,其ID
分别设置为IDC_SYNCHRONIZE、IDC_SHOWCNTRTHRD 和IDC_PAUSE。列表框用于显示线程显示程序中两个线程的共同操作对象m_strNumber,其ID
设置为IDC_DATABOX。
3 创建类CWinThread 的派生类CExampleThread。该类将作为本程序中使用的两个线程类:CCounterThread
和CDisplayThread
的父类。这样做的目的仅是为了共享两个线程类的共用变量和函数。
在CExampleThread 的头文件中填加如下变量:
CMutexesDlg * m_pOwner;// 指向类CMutexesDlg 指针
BOOL m_bDone;// 用以控制线程执行
及函数:
void SetOwner(CMutexesDlg* pOwner)
{ m_pOwner=pOwner; };// 取类CMutexesDlg 的指针
然后在构造函数当中对成员变量进行初始化:
m_bDone=FALSE;// 初始化允许线程运行
m_pOwner=NULL;// 将该指针置为空
m_bAutoDelete=FALSE;//
要求手动删除线程对象
4 创建两个线程类CCounterThread 和CdisplayThread。这两个线程类是CExampleThread
的派生类。分别重载两个线程函数中的::Run()
函数,实现各线程的任务。在这两个类当中分别加入同步访问类的锁对象sLock,这里将根据同步机制的复选与否来确定是否控制对共享资源的访问。不要忘记需要加入头文件#include
"afxmt.h"。
计数线程::Run() 函数的重载代码为:
int CCounterThread::Run()
{
BOOL fSyncChecked;// 同步机制复选检测
unsigned int nNumber;// 存储字符串中整数
if (m_pOwner == NULL)
return -1;
// 将同步对象同锁对象联系起来
CSingleLock sLock(&(m_pOwner- > m_mutex));
while (!m_bDone)// 控制线程运行,为终止线程服务
{
// 取同步机制复选状态
fSyncChecked =
m_pOwner- > IsDlgButtonChecked(IDC_SYNCHRONIZE);
// 确定是否使用同步机制
if (fSyncChecked)
sLock.Lock();
// 读取整数
_stscanf((LPCTSTR) m_pOwner- > m_strNumber, _T("%d"), &nNumber);
nNumber++;// 加1
m_pOwner- >m_strNumber.Empty();// 字符串置空
while (nNumber != 0) // 更新字符串
{
m_pOwner- > m_strNumber += (TCHAR) ('0'+nNumber%10);
nNumber /= 10;
}
// 调整字符串顺序
m_pOwner- >m_strNumber.MakeReverse();
// 如果复选同步机制,释放资源
if (fSyncChecked)
sLock.Unlock();
// 确定复选显示计数线程
if (m_pOwner- >IsDlgButtonChecked(IDC_SHOWCNTRTHRD))
m_pOwner- >AddToListBox(_T("Counter:
Add 1"));
}// 结束while
m_pOwner- >PostMessage(WM_CLOSE, 0, 0L);
return 0;
}
显示线程的::Run() 函数重载代码为:
int CDisplayThread::Run()
{
BOOL fSyncChecked;
CString strBuffer;
ASSERT(m_pOwner != NULL);
if (m_pOwner == NULL)
return -1;
CSingleLock sLock(&(m_pOwner- >m_mutex));
while (!m_bDone)
{
fSyncChecked = m_pOwner- >
IsDlgButtonChecked(IDC_SYNCHRONIZE);
if (fSyncChecked)
sLock.Lock();
// 构建要显示的字符串
strBuffer = _T("Display: ");
strBuffer += m_pOwner- >m_strNumber;
if (fSyncChecked)
sLock.Unlock();
// 将字符串加入到列表框中
m_pOwner- >AddToListBox(strBuffer);
}// 结束while
m_pOwner- >PostMessage(WM_CLOSE, 0, 0L);
return 0;
}
3 在CMutexesDlg 的头文件中加入如下成员变量:
CString m_strNumber;//
线程所要操作的资源对象
CMutex m_mutex;//
用于同步机制的互斥量
CCounterThread* m_pCounterThread;//
指向计数线程的指针
CDisplayThread* m_pDisplayThread;//
指向显示线程的指针
首先在对话框的初始化函数中加入如下代码对对话框进行初始化:
BOOL CMutexesDlg::OnInitDialog()
{
......
// 初始化进程优先级组合框并置缺省为NORMAL
CComboBox* pBox;
pBox = (CComboBox*) GetDlgItem(IDC_PRIORITYCLASS);
ASSERT(pBox != NULL);
if (pBox != NULL){
pBox- >AddString(_T("Idle"));
pBox- >AddString(_T("Normal"));
pBox- >AddString(_T("High"));
pBox- >AddString(_T("Realtime"));
pBox- >SetCurSel(1);
}
// 初始化显示线程优先级组合框并置缺省为NORMAL
pBox = (CComboBox*) GetDlgItem(IDC_DSPYTHRDPRIORITY);
ASSERT(pBox != NULL);
if (pBox != NULL){
pBox- >AddString(_T("Idle"));
pBox- >AddString(_T("Lowest"));
pBox- >AddString(_T("Below normal"));
pBox- >AddString(_T("Normal"));
pBox- >AddString(_T("Above normal"));
pBox- >AddString(_T("Highest"));
pBox- >AddString(_T("Timecritical"));
pBox- >SetCurSel(3);
}
// 初始化计数线程优先级组合框并置缺省为NORMAL
pBox = (CComboBox*) GetDlgItem(IDC_CNTRTHRDPRIORITY);
ASSERT(pBox != NULL);
if (pBox != NULL){
pBox- >AddString(_T("Idle"));
pBox- >AddString(_T("Lowest"));
pBox- >AddString(_T("Below
normal"));
pBox- >AddString(_T("Normal"));
pBox- >AddString(_T("Above
normal"));
pBox- >AddString(_T("Highest"));
pBox-
>AddString(_T("Timecritical"));
pBox- >SetCurSel(3);
}
// 初始化线程挂起复选框为挂起状态
CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE);
pCheck- >SetCheck(1);
// 初始化线程
m_pDisplayThread = (CDisplayThread*)
AfxBeginThread(RUNTIME_CLASS(CDisplayThread),
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
m_pDisplayThread- >SetOwner(this);
m_pCounterThread = (CCounterThread*)
AfxBeginThread(RUNTIME_CLASS(CCounterThread),
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
m_pCounterThread- >SetOwner(this);
......
}
然后填加成员函数:
void AddToListBox(LPCTSTR szBuffer);// 用于填加列表框显示
该函数的实现代码为:
void CMutexesDlg::AddToListBox(LPCTSTR szBuffer)
{
CListBox* pBox = (CListBox*) GetDlgItem(IDC_DATABOX);
ASSERT(pBox != NULL);
if (pBox != NULL){
int x = pBox- >AddString(szBuffer);
pBox- >SetCurSel(x);
if (pBox- >GetCount() > 100)
pBox-
>DeleteString(0);
}
}
然后利用ClassWizard
填加用于调整进程优先级、两个线程优先级以及用于复选线程挂起的函数。
调整进程优先级的代码为:
void CMutexesDlg::OnSelchangePriorityclass()
{
DWORD dw;
// 取焦点选项
CComboBox* pBox = (CComboBox*) GetDlgItem(IDC_PRIORITYCLASS);
int nCurSel = pBox- >GetCurSel();
switch (nCurSel)
{
case 0:
dw = IDLE_PRIORITY_CLASS;break;
case 1:
default:
dw = NORMAL_PRIORITY_CLASS;break;
case 2:
dw = HIGH_PRIORITY_CLASS;break;
case 3:
dw = REALTIME_PRIORITY_CLASS;break;
}
SetPriorityClass(GetCurrentProcess(), dw);// 调整优先级
}
由于调整两个线程优先级的代码基本相似,单独设置一个函数根据不同的ID
来调整线程优先级。该函数代码为:
void CMutexesDlg::OnPriorityChange(UINT nID)
{
ASSERT(nID == IDC_CNTRTHRDPRIORITY ||
nID == IDC_DSPYTHRDPRIORITY);
DWORD dw;
// 取对应该ID 的焦点选项
CComboBox* pBox = (CComboBox*) GetDlgItem(nID);
int nCurSel = pBox- >GetCurSel();
switch (nCurSel)
{
case 0:
dw = (DWORD)THREAD_PRIORITY_IDLE;break;
case 1:
dw = (DWORD)THREAD_PRIORITY_LOWEST;break;
case 2:
dw = (DWORD)THREAD_PRIORITY_BELOW_NORMAL;break;
case 3:
default:
dw = (DWORD)THREAD_PRIORITY_NORMAL;break;
case 4:
dw = (DWORD)THREAD_PRIORITY_ABOVE_NORMAL;break;
case 5:
dw = (DWORD)THREAD_PRIORITY_HIGHEST;break;
case 6:
dw =
(DWORD)THREAD_PRIORITY_TIME_CRITICAL;break;
}
if (nID == IDC_CNTRTHRDPRIORITY)
m_pCounterThread- >SetThreadPriority(dw);//
调整计数线程优先级
else
m_pDisplayThread- >SetThreadPriority(dw);//
调整显示线程优先级
}
这样线程优先级的调整只需要根据不同的ID 来调用该函数:
void CMutexesDlg::OnSelchangeDspythrdpriority() { OnPriorityChange(IDC_DSPYTHRDPRIORITY);}
void CMutexesDlg::OnSelchangeCntrthrdpriority()
{ OnPriorityChange(IDC_CNTRTHRDPRIORITY);}
复选线程挂起的实现代码如下:
void CMutexesDlg::OnPause()
{
// 取挂起复选框状态
CButton* pCheck = (CButton*)GetDlgItem(IDC_PAUSE);
BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0);
if (bPaused) {
m_pCounterThread- >SuspendThread();
m_pDisplayThread- >SuspendThread();
}// 挂起线程
else {
m_pCounterThread- >ResumeThread();
m_pDisplayThread- >ResumeThread();
}// 恢复线程运行
}
程序在::OnClose()
中实现了线程的终止。在本例程当中对线程的终止稍微复杂些。需要注意的是成员变量m_bDone
的作用,在线程的运行当中循环检测该变量的状态,最终引起线程的退出。这样线程的终止是因为函数的退出而自然终止,而非采用强行终止的方法,这样有利于系统的安全。该程序中使用了PostMessage
函数,该函数发送消息后立即返回,这样可以避免阻塞。其实现的代码为:
void CMutexesDlg::OnClose()
{
int nCount = 0;
DWORD dwStatus;
// 取挂起复选框状态
CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE);
BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0);
if (bPaused == TRUE){
pCheck- >SetCheck(0);// 复选取消
m_pCounterThread- >ResumeThread();//
恢复线程运行
m_pDisplayThread- >ResumeThread();
}
if (m_pCounterThread != NULL){
VERIFY(::GetExitCodeThread(m_pCounterThread- >m_hThread, &dwStatus));
// 取计数线程结束码
if (dwStatus == STILL_ACTIVE){
nCount++;
m_pCounterThread-
>m_bDone = TRUE;
}// 如果仍为运行状态,则终止
else{
delete
m_pCounterThread;
m_pCounterThread =
NULL;
}//
如果已经终止,则删除该线程对象
}
if (m_pDisplayThread != NULL){
VERIFY(::GetExitCodeThread(m_pDisplayThread- >m_hThread, &dwStatus));
// 取显示线程结束码
if (dwStatus == STILL_ACTIVE){
nCount++;
m_pDisplayThread-
>m_bDone = TRUE;
}// 如果仍为运行状态,则终止
else{
delete
m_pDisplayThread;
m_pDisplayThread =
NULL;
}//
如果已经终止,则删除该线程对象
}
if (nCount == 0)// 两个线程均终止,则关闭程序
CDialog::OnClose();
else // 否则发送WM_CLOSE 消息
PostMessage(WM_CLOSE, 0, 0);
}
在例程具体实现中用到了许多函数,在这里不一一赘述,关于函数的具体意义和用法,可以查阅联机帮助。
