9.2 线程的操作技巧
Windows是一种多任务的操作系统,在Windows的一个进程内包含一个或多个线程。在32位Windows环境下的Win32 API提供了多线程应用程序开发所需要的接口函数,而利用VC++中提供的标准C库也可以开发多线程应用程序,相应的MFC类库封装了多线程编程的类,用户在开发时可根据应用程序的需要和特点选择相应的工具。为了使大家能全面地了解Windows多线程编程技术,本文将重点介绍在Win32 API和MFC两种方式下如何编制多线程程序。
多线程编程在Win32方式下和在MFC类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程。当线程执行完后,自动终止线程;当进程结束后,所有的线程都终止。所有活动的线程共享进程的资源,因此,在编程时需要考虑在多个线程访问同一资源时产生冲突的问题。当一个线程正在访问某进程对象,而另一个线程要改变该对象时,就可能会产生错误的结果,编程时要解决这个冲突。
9.2.1 线程的概念
理解线程是非常关键的,因为每个进程至少需要一个线程。本节将更加详细地介绍线程的知识,尤其是要讲述进程与线程之间存在的差别,它们各自具有什么作用。还要介绍系统如何使用线程内核对象来管理线程,与进程内核对象一样,线程内核对象也拥有属性,我们将要观察许多用于查询和修改这些属性的函数。此外还要介绍可以在进程中创建和生成更多的线程时所用的函数。
上一节介绍的进程是由两个部分构成的,一个是进程内核对象,另一个是地址空间。同样,线程也是由两个部分组成的:
l 线程的内核对象,操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
l 线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。
上一节中讲过,进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个生命期都在该进程中。这意味着线程在它的进程地址空间中执行代码,并且在进程的地址空间中对数据进行操作。因此,如果在单进程环境中,有两个或多个线程正在运行,那么这两个线程将共享单个地址空间。这些线程能够执行相同的代码,对相同的数据进行操作。这些线程还能共享内核对象句柄,因为句柄表依赖于每个进程而不是每个线程。
9.2.2 创建/终止线程的技巧
1.问题阐述
线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件、信号标识及动态分配的内存等。一个进程内的所有线程使用同一个地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行及什么时候执行线程,线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完后再执行。在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样可使处理器任务平衡,并提高系统的运行效率。
2.实现技巧
创建用户界面线程有两种方法。第一种方法,首先从CWinTread类派生一个类(注意,必须要用宏DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE对该类进行声明和实现);然后调用函数AfxBeginThread创建CWinThread派生类的对象进行初始化,启动线程运行;第二种方法,先通过构造函数创建类CWinThread的一个对象,然后由程序员调用函数::CreateThread来启动线程。
调用CreateProcess函数创建新的进程,运行指定的程序。
CreateProcess的原型如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation );
终止一个线程有两种方法:最常用的方法是调用函数ExitThread()结束线程;另一种方法是调用函数TerminateThread终止线程。在当前线程中的一个线程调用函数ExitProcess就会结束当前线程:
VOID ExitThread(DWORD dwExitCode);
这个函数用来结束当前线程,其中参数用来存放此线程的退出码,这是最正常的结束线程的方法:
BOOL TerminateThread(
HANDLE hThread. // 线程句柄
DWORD dwExitCode // 线程退出码
);
3.实例代码
#include <windows.h>
#include <iostream.h>
DWORD WINAPI ThreadFunc(HANDLE Thread)
{
int i;
for(i=0;i<10;i++)
{
cout<<"A new thread has created!"<<endl;
}
return 0;
}
int main(int argc,char* argv[])
{
HANDLE Thread;
DWORD dwThreadId;
Thread=::CreateThread
(NULL,0,ThreadFunc,NULL,0,&dwThreadId);
cout<<"The new thread ID is :"<<dwThreadId<<endl;
::WaitForSingleObject(Thread,INFINITE);
::CloseHandle(Thread);
return 0;
}
4.小结
我们知道,要创建一个线程,必须得有一个主进程,然后由这个主进程来创建一个线程,在一般的VC程序中,主函数所在的进程就是程序的主进程。
9.2.3 工作线程实现的技巧
1.问题阐述
工作线程是用于处理后台工作的,我们平常接触到的后台打印就是一个工作线程的例子。下面我们看看如何创建一个工作线程。
创建一个工作线程十分简单,只需要两步:实现线程函数和开始线程。不需要由CWinThread派生类,你可以不加修改地使用CWinThread。
AfxBeginThread有两种形式,一种是用来创建用户界面线程的,另一种就是用来创建工作线程的。为了开始执行线程,只需要向AfxBeginThread提供下面的参数就可以了。
l 线程函数的地址。
l 传送到线程函数的参数。
l (可选的)线程的优先级,默认的是平常的优先级,如果希望使用其他优先级请参阅::SetThreadPriority。
l (可选的)线程的堆栈大小,默认的大小是和创建线程的堆栈一样大。
l (可选的)如果用户创建的线程在开始的时候处于挂起态,而不在运行态,可以设置为CREATE_SUSPENDED。
l (可选的)线程的安全属性,默认的是和父线程的访问权限一样,有关安全信息的格式,请参阅SECURITY_ATTRIBUTES。
2.实现技巧
AfxBeginThread为用户创建并初始化一个CWinThread对象,运行这个对象,并返回它的地址,这样通过这个地址用户就可以找到它了。在这一过程中还要进行许多检查,这一切都不用你操心。AfxBeginThread函数的声明如下:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
那么下面我们来看看线程函数怎么写。线程函数定义了线程要做什么,在进入这个函数的时候线程开始,退出的时候线程结束。这个函数必须是下面的形式:
UINT ControllingFunction( LPVOID pParam );
参数是一个32位数,这个参数是在线程对象创建时传送给对象的构造函数。至于线程函数要怎么处理这个数,那就随便了,它可能是一个人的年纪,可能是一个文件的地址,可能是一个窗口句柄,反正你想它是什么就是什么,主动权在你手里。如果参数指的是一个结构,可以用来向线程传送参数,也可以让线程把结果传回主程序,线程需要通知主程序,什么时候来取结果。
在线程函数结束时,应该返回一个UINT类型的值,说明返回原因,也就是返回代码。通常这个数为0,表示正常返回,当然你也可以定义一个错误编码指示错误了。
3.实例代码
下面是一个线程函数的例子,这个例子解释如何定义线程函数,也介绍了如何从程序的其他地方控制线程:
UINT ThreadProc( LPVOID pParam )
{
return 0; // 线程成功完成
}
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc, // 线程函数地址
LPVOID pParam, // 线程参数
int nPriority = THREAD_PRIORITY_NORMAL, // 线程优先级
UINT nStackSize = 0, // 线程堆栈大小,默认为1M
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
4.小结
工作线程通常用于程序的计算、调度等后台任务。工作线程和用户界面线程不同,它不从CWinThread类派生创建,最重要的是实现完成工作线程任务的运行控制函数,即工作线程常表现为函数,这个函数完成线程并行的任务,由其他语句调用工作线程函数将线程启动。
9.2.4 用户界面线程实现的技巧
1.问题阐述
MFC中有两类线程,分别称为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。
工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。
2.实现技巧
当一个Windows应用程序运行时,它会自动产生一个主线程,一般的窗口处理等都由该主线程处理,在主线程中可以创建和使用其他线程。用户界面线程通常用于处理用户输入并响应各种事件和消息。
启用用户界面线程的函数AfxBeginThread()与启用工作线程的函数是同一个函数的不同重载形式,该函数的声明如下:
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
l pParam:传递给线程函数的一个32位参数,执行函数将用某种方式解释该值。它可以是数值,或指向一个结构的指针,甚至可以被忽略。
l nPriority:线程的优先级。如果为0,则线程与其父线程具有相同的优先级。
l nStackSize:线程为自己分配堆栈的大小,其单位为字节。如果nStackSize被设为0,则线程的堆栈被设置成与父线程堆栈大小相同。
l dwCreateFlags:如果为0,则线程在创建后立刻开始执行;如果为CREATE_ SUSPEND,则线程在创建后立刻被挂起。
l lpSecurityAttrs:线程的安全属性指针,一般为NULL。
3.实例代码
BOOL CMyThread::InitInstance()
{
GetMainWnd()->SetWindowText("从线程中设置标题栏文字");
return TRUE;
}
// CTestView 消息处理程序
#include"MyThread.h"
void CTestView::OnRButtonDown(UINT nFlags, CPoint point)
{
CMyThread *pThread;
pThread=(CMyThread*)AfxBeginThread(RUNTIME_CLASS(CMyThread));
CView::OnRButtonDown(nFlags, point);
}
4.小结
在Visual C++ 6.0编程环境中,我们既可以编写C风格的32位Win32应用程序,也可以利用MFC类库编写C++风格的应用程序,二者各有其优缺点。基于Win32的应用程序执行代码小巧,运行效率高,但要求程序员编写的代码较多,且需要管理系统提供给程序所有资源;而基于MFC类库的应用程序可以快速建立起应用程序,类库为程序员提供了大量的封装类,而且Developer Studio为程序员提供了一些工具来管理用户源程序,其缺点是类库代码很庞大。由于使用类库所带来的快速、简捷和功能强大等优越性,因此除非有特殊的需要,否则Visual C++推荐使用MFC类库进行程序开发。
9.2.5 使用事件对象完成线程的同步的技巧
1.问题阐述
CEvent类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为A)负责监听通信端口,另外一个线程(记为B)负责更新用户数据。通过使用CEvent类,线程A可以通知线程B何时更新用户数据。每一个CEvent对象可以有两种状态:有信号状态和无信号状态。线程监视位于其中的CEvent类对象的状态,并在相应的时候采取相应的操作。
2.实现技巧
在MFC中,CEvent 类对象有两种类型:人工事件和自动事件。一个自动CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放可利用线程,但直到调用成员函数ReSetEvent()才将其设置为无信号状态。在创建CEvent 类的对象时,默认创建的是自动事件。CEvent 类的各成员函数的原型和参数说明如下:
CEvent(BOOL bInitiallyOwn=FALSE,
BOOL bManualReset=FALSE,
LPCTSTR lpszName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);
l bInitiallyOwn:指定事件对象初始化状态,TRUE为有信号,FALSE为无信号。
l bManualReset:指定要创建的事件是属于人工事件还是自动事件,TRUE为人工事件,FALSE为自动事件。
BOOL CEvent::SetEvent();
将CEvent类对象的状态设置为有信号状态。如果事件是人工事件,则CEvent类对象保持为有信号状态,直到调用成员函数ResetEvent()将其重新设为无信号状态时为止。如果CEvent类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent类对象由系统自动重置为无信号状态。
如果该函数执行成功,则返回非零值,否则返回零。
BOOL CEvent::ResetEvent();
该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置的,故自动事件不需要调用该函数。如果该函数执行成功,返回非零值,否则返回零。我们一般通过调用WaitForSingleObject函数来监视事件状态。
3.实例代码
#include <windows.h>
#include <iostream.h>
DWORD WINAPI Fun1Proc(
LPVOID lpParameter // 线程数据
);
DWORD WINAPI Fun2Proc(
LPVOID lpParameter // 线程数据
);
int tickets=100;
HANDLE g_hEvent;
void main()
{
HANDLE hThread1;
HANDLE hThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
//g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
g_hEvent=CreateEvent(NULL,FALSE,FALSE,"tickets");
if(g_hEvent)
{
if(ERROR_ALREADY_EXISTS==GetLastError())
{
cout<<"only instance can run!"<<endl;
return;
}
}
SetEvent(g_hEvent);
Sleep(4000);
CloseHandle(g_hEvent);
}
DWORD WINAPI Fun1Proc(
LPVOID lpParameter // 线程数据
)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
ResetEvent(g_hEvent);
if(tickets>0)
{
Sleep(1);
cout<<"thread1 sell ticket : "<<tickets--<<endl;
}
else
break;
SetEvent(g_hEvent);
}
return 0;
}
DWORD WINAPI Fun2Proc(
LPVOID lpParameter // 线程数据
)
{
while(TRUE)
{
WaitForSingleObject(g_hEvent,INFINITE);
ResetEvent(g_hEvent);
if(tickets>0)
{
Sleep(1);
cout<<"thread2 sell ticket : "<<tickets--<<endl;
}
else
break;
SetEvent(g_hEvent);
}
return 0;
}
4.小结
事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。有两种不同类型的事件对象:一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
9.2.6 使用信号量完成线程的同步的技巧
1.问题阐述
当需要一个计数器来限制可以使用某个线程的数目时,可以使用“信号量”对象。CSemaphore类的对象保存了对当前访问某一指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程的数目。如果这个计数达到了零,则所有对这个CSemaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零时为止。一个线程被释放已访问了被保护的资源时,计数值减1;一个线程完成了对被控共享资源的访问时,计数值增1。这个被CSemaphore类对象所控制的资源可以同时接收访问的最大线程数在该对象的构建函数中指定。
2.实现技巧
CSemaphore类的构造函数原型及参数说明如下:
CSemaphore (LONG lInitialCount=1,
LONG lMaxCount=1,
LPCTSTR pstrName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);
l lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值。
l lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目。
后两个参数在同一进程中使用一般为NULL,不做过多讨论。
3.实例代码
声明3个线程函数:
UINT WriteA(LPVOID pParam);
UINT WriteB(LPVOID pParam);
UINT WriteC(LPVOID pParam);
定义信号量对象和一个字符数组,为了能够在不同线程间使用,定义为全局变量:
CSemaphore semaphoreWrite(2,2); //资源最多访问线程2个,当前可访问线程数2个
char g_Array[10];
添加3个线程函数:
UINT WriteA(LPVOID pParam)
{
CEdit *pEdit=(CEdit*)pParam;
pEdit->SetWindowText("");
WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE);
CString str;
for(int i=0;i<10;i++)
{
pEdit->GetWindowText(str);
g_Array[i]=''A'';
str=str+g_Array[i];
pEdit->SetWindowText(str);
Sleep(1000);
}
ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL);
return 0;
}
UINT WriteB(LPVOID pParam)
{
CEdit *pEdit=(CEdit*)pParam;
pEdit->SetWindowText("");
WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE);
CString str;
for(int i=0;i<10;i++)
{
pEdit->GetWindowText(str);
g_Array[i]=''B'';
str=str+g_Array[i];
pEdit->SetWindowText(str);
Sleep(1000);
}
ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL);
return 0;
}
UINT WriteC(LPVOID pParam)
{
CEdit *pEdit=(CEdit*)pParam;
pEdit->SetWindowText("");
WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE);
for(int i=0;i<10;i++)
{
g_Array[i]=''C'';
pEdit->SetWindowText(g_Array);
Sleep(1000);
}
ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL);
return 0;
}
这3个线程函数不再多说,在信号量对象有信号的状态下,线程执行到WaitForSingleObject语句处继续执行,同时可用线程数减1;若线程执行到WaitForSingleObject语句时信号量对象无信号,线程就在这里等待,直到信号量对象有信号线程才往下执行。
添加其响应函数:
void CMultiThread10Dlg::OnStart()
{
CWinThread *pWriteA=AfxBeginThread(WriteA,
&m_ctrlA,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
pWriteA->ResumeThread();
CWinThread *pWriteB=AfxBeginThread(WriteB,
&m_ctrlB,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
pWriteB->ResumeThread();
CWinThread *pWriteC=AfxBeginThread(WriteC,
&m_ctrlC,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
pWriteC->ResumeThread();
}
4.小结
在用CSemaphore 类的构造函数创建信号量对象时要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许其他线程进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源数加1。
9.2.7 使用互斥量完成线程的同步的技巧
1.问题阐述
互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
互斥对象与临界区对象很像,互斥对象与临界区对象的不同在于:互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。当然,互斥对象也可以用于同一进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率。
9.2.8 使用临界量完成线程的同步的技巧
1.问题阐述
当多个线程访问一个独占性共享资源时,可以使用“临界区”对象。任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其他希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。
2.实现技巧
待续。。。。