Windows系统平台经历了16位到32位的转变后,系统运行方式和任务管理方式有了很大的变化,在Windows 95和Windows NT中,每个Win32程序在独立的进程空间上运行,32位地址空间使我们从16位段式结构的64K段限制中摆脱出来,逻辑上达到了4G的线性地址空间,我们在设计程序时,不再需要考虑编译的段模式,同时还提高了大程序的运行效率。独立进程空间的另一个更大的优越性是大大提高了系统的稳定性,一个应用的异常错误不会影响其它的应用。与在MS-DOS和16位Windows操作系统中不同,32位Windows进程是没有活力的。这就是说,一个32位Windows进程并不执行什么指令,它只是占据着4GB的地址空间,此空间中有应用程序EXE文件的代码和数据。EXE需要的DLL也将它们的代码的数据装入到进程的地址空间。除了地址空间,进程还占有某些资源,比如文件、动态内存分配和线程。当进程终止时,在它生命期中创建的各种资源将被清除。
如上所述,进程是没有活力的,它只是一个静态的概念。为了让进程完成一些工作,进程必须至少占有一线程,所以线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含几个线程,它们可以同时执行进程的地址空间中的代码。为了做到这一点,每个线程有自己的一组CPU寄存器和椎。每个进程至少有一个线址程在执行其地址空间中的代码,如果没有线程执行进程地空间中的代码,如果没有线程执行进程地址空间中的代码,进程也就没有继续存在的理由,系统将自动清除进程及其地址空间。为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮转方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。创建一个32位Windows进程时,它的第一个线程称为主线程,由系统自动生成,然后可由这个主线程生成额外的线程,这些线程又可生成更多的线程。
例如,在基于Internet网上的可视电话系统中,同时要进行语音采集、语音编译码、图像采集、图像编译码、语音和图像码流的传输,所有这些工作,都要并行处理。特别是语音信号,如果进行图像编解码时间过长,语音信号得不到服务,通话就有间断;如果图像或语音处理时间过长,而不能及时传输码流数据,通信同样也会中断。这样就要求我们实现一种并行编程,在只有一个CPU的机器上,也就是要将该CPU时间按时一定的优先准则分配给各个事件,定期处理各事件,而不会对某一事件处理过长。
在32位Windows95或Windows NT下,我们可以用多线程的处理技术来实现这种并行处理。实际上,这种并行编程在很多场合下都是必须的。再例如,在File Manager拷贝文件时,它显示一个对话框中包含了一个Cancel按钮。如果在文件拷贝过程中,点中Cance l按钮,就会终止拷贝。在16位Winows中,实现这类功能需要在File Copy循环内部周期性地调用PeekMessage函数。如果正在读一个很大的动作;如果从软盘读文件,则要花费好几秒的时间。由于机器反应太迟钝,用户会频繁地点中这个按钮,以为系统不知道想终止这个操作。如果把File Copy指令放入另外一个线程,就不需要在代码中放一大堆PeekMessage函数,处理用户界面的线程将与它分开操作,点中Cancel按钮后会立即得到响应。同样的道理,在应用程序中创建一个单独线程来处理所有打印任务也是很有用的,用户可以在打印处理时继续使用应用程序。
多线程的编程在Win32方式下和MFC类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程,当线程执行完任务后,自动终止线程,当进程结束后,所有的线程都终止。所有活动的线程共享进程的资源,所以在编程时,需要考虑在多个线程访问同一资源时,产生冲突的问题,当一个线程正在访问一个进程对象,这时另一个线程要改变该对象,这时可能会产生错误的结果,所以程序员在编程时要解决这种冲突。
下面举例说明在Win32 基础上进行多线程编程的过程。
1.使用函数说明
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. 无优先级例程,该例程实现在对话框上通过一线程实现时钟的显示和停止。步骤如下:
第一步:建立一基于对话框的工程MultiProcess1。
第二步:在对话框上建立两个按钮和一个编辑框,ID号分别为ID_START、ID_STOP和IDC_TIME,Caption分别为"启动"、"停止"。如下:
第三步:在MultiProcess1Dlg.cpp中定义全局函数ThreadProc(),格式如下:
void ThreadProc()
{
CTime time;
CString m_time;
for(;;)
{
time=CTime::GetCurrentTime();
m_time=time.Format("%H:%M:%S");
::SetDlgItemText(AfxGetMainWnd()->m_hWnd,IDC_TIME,m_time);
Sleep(1000);
}
}
第四步:在头文件MultiProcess1Dlg.h中定义变量如下:
DWORD ThreadID;
HANDLE hThread;
第五步:双击"开始"按钮,生成消息映射函数OnStart(),编写其中的代码如下:
void CMultiProcess1Dlg::OnStart()
{
// TODO: Add your control notification handler code here
hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,
NULL,0,&ThreadID);
}
此时即刻实现在对话框上点击"启动",启动时钟。接下来我们实现如何让时钟停下来。
第六步:双击"停止"按钮,添加停止的消息映射函数OnStop(),编写代码如下:
void CMultiProcess1Dlg::OnStop()
{
// TODO: Add your control notification handler code here
TerminateThread(hThread,1);
}
注意:该函数可能会引起系统不稳定,而且,线程所占用的资源也不释放,因此,一般情况下,建议不要使用该函数。
到现在,这个程序就完整了,看一下效果吧!
最后需要说明的是,并不是设计多线程就是一个好的程序。目前大多数的计算机都是单处理器(CPU)的,在这种机器上运行多线程程序,有时反而会降低系统性能,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,但对于大部分时间被阻塞的线程(例如等待文件 I/O 操作),可以用一个单独的线程来完成,这样可把CPU时间让出来,使程序获得更好的性能。因此,在设计多线程应用时,需要慎重选择,具体情况具体处理,以使应用程序获得最佳的性能。