您的当前位置:首页多线程编程

多线程编程

来源:小侦探旅游网
第章 多线程编程

Windows 是一个多任务操作系统。Windows 95/NT 实行的是抢先式多任务。在 Windows 中每个进程可同时执行多个线程,这意味着一个程序可以同时完成多个任务。例如,对于既要进行耗时工作、又要保持对用户输入响应的通信应用程序来说,使用多线程是最佳选择。当进程使用多个线和时,需要采取适当的措施来保持线程之间的同步。本章讲解的知识点包括:

■ 进程和线程的基本概念; ■ 线程技术; ■ 线程操作。

通过本章学习,读者可以掌握多进程、多线程的基本概念,掌握线程的基本操作和多线程编程的基本操作步骤。

14.1 多任务、进程和线程

Windows 是一个多任务操作系统,它允许用户同时执行多个任务,例如,用户可以边听歌曲边上网浏览网页,这就是多任务的情况。那么进程与线程又是怎样的关系呢?下面进行基本概念的讲解。

14.1.1 多任务介绍

多任务的概念人们已经比较熟悉了,它是指用户可在同一时间内运行多个应用程序,每个应用程序都被称作一个任务。前面已经介绍了,Windows 系统就是一个支持多任务的操作系统。本书中的多任务应用也主要是对 Windows 下多任务的具体应用。

Windows 多任务处理采用的是所谓的虚拟机(Virtual Machine)的技术。对于同一时间段内运行的多个程序,系统为它们每个都分配一定的时间片。这样多个程序在一个时间段内被多次执行和停顿。由于每个程序分到的时间段非常短,也就是说程序运行周期比较短。这样宏观上看,每个程序都可以看做是实时运行的,也就是多个程序同时在运行。

随着技术的发展,CPU 不仅仅是简单地对每个应用程序分配时间片,而且还可对任务进行控制。这就是所谓的“抢占式”任务的依据。在某个时间段内运行的多个程序,可能因各程序的重要性不同、数据处理的复杂程度不同,因而 CPU 就需要动态地对每个任务进行控制。例如,对于较为重要的任务,CPU可能会分配较多的时间段和较多的资源。

14.1.2 进程

所谓进程就是应用程序的执行实例,简单地说就是运行中的程序,它是应用程序的动态描述。一个程序没有运行时,它仅仅是一个程序;当用户让程序运行起来,就产生一个进程。每个进程产生时都有自己私有的虚拟地址空间,在 Windows XP 中,系统会为每个进程分配 4GB 的虚拟内存空间。同时每个进程还拥有操作系统分配的一组资源。

操作系统要对进程及其资源进行管理,把进程的相关信息保存在一个称为进程控制块(PCB)的存储区域中,操作系统利用进程控制块来管理进程。

说明:程序是指令的有序集合,其本身没有任何运行的含义,完全是一个静态的概念。而程序在处理机上的一次执行过程(即进程),则是一个动态的概念。进程具有创建其它进程的功能,而程序则没有;同时进程可以关闭,即退出程序的执行。

14.1.3 单线程与多线程

所谓的进程是指当前正在系统上运行的每一个程序的实例,而每个进程又包含一到多个线程。进程是一个应用程序的实例,而线程则是进程的一个执行单元。所谓执行单元,就是一组 CPU 指令、寄存器和堆栈。线程利用进程的资源,它本身不另外拥有任何资源。当然线程也可以是一段程序指令,在高级语言中它可以是完成某种功能的程序段或函数。

单线程与多线程的具体关系分类如下:

1、单线程模型

在这种线程模型中一个进程中只能有一个线程,也就是说整个应用程序完全按照线程的执行顺序运行。剩下的线程必续等到当前线程执行完毕才能运行。这种模型的缺点在于完成小任务也必须占用很长时间,同时如果进程中的线程执行受阻,整个程序就会暂停执行。

2、多线程模型

多线程模型是每个进程内有多个线程,这样可以加快程序的运行速度。在一些程序中,例如端口扫描等,使用多线程模型可以明显加快程序的执行速度。在多线程模型中,每个应用程序都可以同时运行多个线程,即可以同时执行多个可执行语句。

上面介绍了多任务、进程和线程的基本概念,可以看到线程是程序运行时可处理的基本单元,下面将会介绍多线程的应用。

14.2 多线程技术

线程是 CPU 调度的基本单位。如果系统拥有多个 CPU,一个进程就可以创建多个线程,这样应用程序就可以充分利用系统的资源。下面将对线程的一些编程技术进行介绍。

14.2.1 线程的创建与终止

在一个应用程序进程中,用户可以创建一个线程来完成一些操作。当应用程序开始运行时,会存在一个线程,称为主线程。主线程是系统自动生成的,接着主线程可生成其它的线程,即创建新线程。表面上看这些线程是同时运行的,但实际并非如此。为了运行这些线程,系统会为每个独立的线程安排 CPU 时间,即线程通过获取 CPU 时间片的方式运行。

当线程完成其任务或当进程退出时,线程应该终止,这时可释放一些资源(如 DLL)以免出现错误。

14.2.2 线程的分类

在 MFC 中将线程分为两种,一种是用户界面线程,另一种是工作线程,下面分别进行介绍。

1、用户界面线程

用户界面线程通常是与一个窗体相联系的线程。这种线程拥有一个消息循环,可以对自己的消息进行处理。如果应用程序有多个窗体(界面),并要对每个窗体进行控制,就可以使用用户界面线程进行窗体的创建和管理。

2、工作线程

工作线程通常用于执行后台数据的处理、统计等,它一般不需要与用户进行交互。它与用户界面线程不同之处在于,它仅在后台运行、执行一些数据处理工作,没有消息循环、不能对用户的操作进行响应。

MFC 对两类线程的创建和操作进行了封装,利用类向导可以方便地添加这些线程类,通过这些类

就可以添加线程的一些函数。

14.2.3 进程的控制

一个进程除了创建主线程以外还可以创建多个线程。当系统需要同时执行多个线程时,就需要用到线程的优先级。线程优先级是指本线程在进程内的相对优先级与本进程优先级的结合。操作系统按线程的优先级调度线程的工作。线程优先级的范围是 1~31,操作系统先将时间片分配给优先级为 31 的线程。等到优先级为 31 的线程全部分配结束,再分配给优先级为 30 的线程,以此依次向下分配。

线程的控制还包括多个线程之间的通信和同步问题,具体的控制方法将在下面进行介绍。

14.3 多线程程序设计

14.3.1 创建线程

在 MFC 中,线程分为用户界面线程和工作线程,二者的创建的过程有所不同。下面分别进行介绍。

1、创建用户界面线程

在 MFC 中把线程功能封装为 CWinThread 类,用类向导所创建的 CWinApp 应用程序类就是从这个类派生出来的。在应用程序中可以用 CWinThread 类来派生用户界面线程,然后调用适当的函数启动该线程。编写用户界线程的步骤如下:

(1)由类向导生成一个新类 CWinApp,其基类设置为 CWinThread; (2)创建并启动线程。可以利用下面两种方法之一创建和启动线程: ■ 利用 AfxBeginThread() 函数,其函数原型如下:

CWinThread *AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority=

THREAD_PRIORITY_MNORMAL, UINT nStackSize=0, DWORD dwCreateFlags=0, LPSECURITY_ATTRIBUTES lpSecurityAttrrs=NULL);

其中,参数 pThreadClass 指定线程的运行类,函数的返回值为线程对象指针。

■ 调用线程类的构造函数来创建线程对象,利用 CWinThread::CreateThread() 函数创建线程。 注意:要利用构造函数创建对象,构造函数必须是 public 成员函数。 2、创建工作线程

创建工作线程实际上就是调用 AfxBeginThread 函数时提供一个函数的地址(即线程的入口)就可以了。工作线程启动之后就进入该函数,并且在函数退出时结束该线程。在 MFC 中可以利用 BeginThread() 函数创建工作线程。函数的原型如下:

CWinThread *AfxBeginThread(AFX_THREADPROC pfnThreadProc,

LPVOID pParam, int nPriority=THREAD_PRIORITY_NORMAL,

UINT nStackSize=0, DWORD dwCreateFlags=0, LPSECURITY_ATTRIBUTES lpSecurityAttrrs=NULL);

此函数各个参数的意义为:

■ pfnThreadProc:函数的入口地址。 ■ pParam:传递给线程的参数。

■ nPriority:线程的优先级。

■ nStackSize:表示栈的大小,为0时表示采用系统值。 ■ dwCreateFlags:创建线程的标记。 ■ lpSecurityAttrs:描述线程的安全属性。

函数的返回值是 CWinThread 类的指针,利用该指针可以实现对线程的控制。

14.3.2 终止线程

当一个线程终止时,应关闭该线程所有属性的全部对象句柄。一般来说,线程的终止包括正常终止和异常终止。正常终止是控制函数到达函数中的结束点,该线程就会终止。对于工作线程来说,也可以利用函数 AfxEndThread() 来终止线程,其函数原型如下:

void AfxEndThread(UINT nExitCode); 其中,参数 nExitCode 表示线程的退出码。

用户界面线程可以通过调用 PostQuitMessage() 函数发送退出消息来结束线程。函数原型如下: void PostQuitMessage(int nExitCode) ;

线程的异常终止是由于线程内部出现无法终止的情况,在其它线程中强行结束该进程。可以调用函数 TerminateThread() 实现异常终止。函数原型如下:

BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

其中,参数 hThread 表示需要终止的线程的句柄,在创建线程时从返回类指针的成员变量中可得到线程的句柄。dwExitCode 表示退出码。如果调用失败,函数返回 0;否则返回非 0 值。需要注意的是,调用该函数后,应该用 CloseHandle 函数来关闭句柄,并释放线程所占用的栈资源。

警告:必须在 CloseHandle() 函数调用之前调用 TerminateThread() 函数,否则会发生无效句柄的错误。

14.3.3 线程间通信

一个线程运行时通常会涉及多个别的线程,因此线程间通信就成了重要的问题。我们举个例子来说明什么是线程通信:若一个应用程序中有两个线程,一个线程负责接收用户输入,另一个负责对前者所提供的数据进行统计,这样就会涉及这两个线程间的通信问题。

一般地说,程序设计中所用的任何通信方式都可以用在线程通信中,如发送消息、内存映射等。一个进程的所有线程都处于此进程的地址空间中,这样线程间的通信就比进程间通信简单得多。

另外,应用程序中的子线程总是要为主线程执行某种任务,这样主线程和子线程间必定要有一个信息传递的渠道,即两个线程间要进行通信。这种通信不但难以避免,而且在多线程编程中也是复杂和频繁的,下面将进行说明。

1、使用全局变量

对于一个进程来说,它可以创建多个线程。每个线程都可以使用系统分配给该进程的所有资源。因此线程之间的通信就可利用一个共享的内容进行,这就是全局变量。使用全局变量时,每个线程都可以改变该变量的值,其它线程就可以访问全局变量中被改变的值。这就是使用全局变量进行通信的基本原理。

2、使用自定义消息

如果一个线程执行中需要对别的线程产生影响,甚至要控制其它线程。此时采用全局变量进行通信已经无法产生这种效用。因为一个线程改变了全局变量后,该线程并无法控制别的线程访问该全局变量的时刻,因此必须采用新的通信机制。在 Windows 程序中采用的是消息驱动方式,利用消息传递就可以控制线程的执行。

所谓的消息通信是指在一个线程的执行函数中,利用 Windows 系统中的消息传递函数向另一个线程发送自定义的消息。即当一个线程发出一条消息时,首先由操作系统接收该消息,然后把该消息转发给目标线程。目标线程可以根据消息的内容做出响应,这就是利用消息进行线程通信的基本原理。

14.4 线程同步

虽然多线程能带来好处,但是也有不少问题需要解决。例如,对于某个数据来说,可能当某线程访问这个数据并对其进行更新时,正巧还有另一线程也在访问该数据,那么这时它使用的是更新前的值还是更新后的值呢?这是需要在程序中进行控制的,这就是所谓的“同步”问题。

使隶属于同一进程的各线程协调一致地工作就称为线程同步。MFC 提供了多种同步对象,下面只介绍最常使用的四种:临界区(CCriticalSection)、事件(CEvent)、互斥量(CMutex)信号量(CSemaphore)。下面就这四种方式分别进行介绍。

1、使用 CCriticalSection 类

当进程包含的多个线程需要访问一个独占性共享资源时,就可以使用“临界区”对象。所谓“临界区”对象就是说,当一个线程获得这样的资源时,在此阶段该线程对它是独占的。只有当线程释放了该资源时,其它线程才可以得到该资源。这样就可保证在同一时刻不会出现多个线程同时访问该共享资源的情况。在 MFC 中采用 CCriticalSection 类对临界区同步进行了封装。该类控制线程同步的基本过程如下:

(1)定义 CCriticalSection 类的一个全局对象,如 CCriticalSection critical_Section; (2)访问需要保护的资源或代码之前,调用 CCriticalSection 类的成员函数 Lock() 锁住临界区对象,这样该线程就唯一地获得了该资源的使用权。

(3)线程对资源使用结束时,可以使用 CCriticalSection 的成员函数 Unlock() 来释放临界区。这时其它的线程就可以再利用 Lock() 函数获取该资源的使用权。

注意:在某线程中用 Lock() 函数获取临界区对象时,如果有其它线程正在访问临界区,则使用Lock() 的线程会立即被挂起,直到临界区被释放。这也是保证临界区被唯一访问的基本原理。

2、使用 CEvent 类

除了用“临界区”保证线程同步外,在 MFC 中还提供了一种事件机制来进行线程同步的控制。所谓的事件机制是指在一个线程中可以利用事件来影响其它线程。在 MFC 中用 CEvent 类对事件同步进行了封装。该类的每个对象都可看作是一个事件。每一个 CEvent 对象(即事件)可以有两种状态:有信号状态和无信号状态。线程监视 CEvent 类对象的状态,当事件对象被某个其它对象释放时才有可能产生动作。

MFC 中将事件为两类:人工事件和自动事件。所谓的自动事件是指线程结束时系统自动将线程中使用的事件对象设置为无信号状态。而人工事件对象则是在调用成员函数 ReSetEvent() 时才将其设置为无信号状态。在创建 CEvent 类的对象时,默认创建的是自动事件。

下面对 CEvent 类的各成员函数的原型和参数说明如下: (1)CEvent 类的构造函数,函数原型如下:

CEvent(BOOL bInitiallyOwn=FALSE, BOOL bManualReset==FALSE, LPCTSTR

lpszName=NULL, LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);

各个参数的意义如下:

■ bInitiallyOwn:指定事件对象初始化状态,TRUE 为有信号,FALSE 为无信号。

■ bManualReset:指定要创建的事件是属于人工事件还是自动事件,TRUE 为人工事件,FALS为自动事件。

(2)SetEvent() 函数原型如下: BOOL CEvent::SetEvent();

将 CEvent 类对象的状态设置为有信号状态。如果该函数执行成功,则返回非零值,否则返回零。 (3)ResetEvent() 函数原型如下: BOOL CEvent::ResetEvent();

该函数将事件的状态设置为无信号状态,并保持该状态直至 SetEvent() 函数被调用时为止。如果该函数执行成功,返回非零值。

由于自动事件由系统自动重置,故自动事件不需要调用该函数。

3、使用 CMutex 类

互斥对象与临界区的含义基本相同。互斥对象可以在进程间使用,也可以用于同一进程的各个线程间使用;而临界区对象只能在同一进程内的各线程间使用。下面讨论的就是在同一进程内的不同线程之间,如何使用互斥对象实现线程同步。

4、使用 CSemaphore 类

当需要一个计数器来限制使用线程的数目时,可以使用“信号量”对象。CSemaphore 类的对象保存了当前还可以使用该资源的线程数目(即访问该资源的线程计数值)。一个线程释放了己被访问的保护资源时,计数值增 1;一个线程使用共享资源时,计数值减 1。由 CSemaphore 类对象控制的资源,可以同时接受多个线程的访问,该资源的最大数量可在该类的构造函数中指定。CSemaphore 类的构造函数原型及参数说明如下:

CSemaphore(LONG lInitialCount=1, LONG lMaxCount=1, LPCTSTR pstrName=NULL,

LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);

■ lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值。

■ lMaxCount:信号量对象的最大计数值,该参数决定了同一时刻可访问受信号量保护资源的最大线程数目。

用 CSemaphore 类的构造函数创建信号量对象时,应指出最大允许资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个访问共享资源的线程,当前可用资源计数就会减 1。只要当前可用资源计数大于 0,就可以发出信号量信号。当可用资源数减到 0 时,就不再允许其它线程进入共享资源了。线程处理完共享资源后离开的同时,应通过 ReleaseSemaphore()

函数将当前可用资源数加 1。

注意:CSemaphore 类包含在头文件 afxmt.h 内,故使用它时须用 #include \"afxmt.h\" 引入该头文件。

上面介绍了线程同步的几种方法。线程的同步是多线程编程中最为重要的要求之一。只有保证了线程的同步,才可以保证这些数据操作是正确的。

14.5 创建线程实例

前面介绍了多线程程序设计的基本知识,本节结合实例对几种多线程程序设计进行介绍。 【示例14.1】在对话框应用程序中创建用户界面线程。操作步骤如下所述: (1)通过 MFC 创建一个基于对话框的应用程序 test。

(2)选择 Insert | New Class 命令,弹出“New Class”对话框,如图14.1 所示。在“Name”文本框中输入新建类名 CMyThread,在“Base class”下拉列表中选择 CwinThread 选项作为基类,单击“OK”按钮。

图14.1 New Class对话框

(3)为了创建一个用户界面,在资源编辑器中添加一个对话框资源,其 ID 为 IDD_DIALOG1,利用类向导为该对话框资源添加新类 CMyDialog。

(4)在 CMyThread 类中添加变量 CMyDialog m_dlg,并且在其初始化函数 InitInstance()中添加下述代码:

BOOL CMyThread::InitInstance(){

// TQDO: perform and per-thread initialization here m_dlg.Create(IDD_DIALOG1);

m_dlg.ShowWindow(SW_SHOW); // 显示对话框 m_pMainWnd=&m_dlg; return TRUE; }

注意:需要在 MyThread.h 文件中添如 #include \"MyDialog.h\" 语句,否则会提示错误。 (5)在 ExitInstance() 函数中添加撤销窗口代码: int CMyThread::ExitInstance(){

// TODO: perform any per-thread cleanup here m_dlg.DestroyWindow(): // 销毁窗口 return CWinThread::ExitInstance(); }

(6)在主对话框中添加一个按钮控件,其 ID 为 IDC_SHOW,标题为“用户界面线程”,添加单击它的消息映射,所编写的函数代码如下:

void CTestDlg::OnShow(){

// TODO: Add your control notification handler code here // 启动用户界面线程

CWinThread *pThread=AfxBeginThread(RUNTIME_CLASS(CMyThread)); }

注意:需要在 TestDlg.cpp 文件中添加 #include \"MyThread.h\" 语句,否则会提示错误。 上面的代码的作用就是启动一个用户界面线程。

(7)利用类向导添加单击的消息映射,编写函数体如下:

void CMyDialog::OnLButtonDown(UINT nFlags, CPoint point){

AfxMessageBox(\"You Clicked The Left Button!\"); // 弹出消息框 CDialog::OnLButtonDown(nFlags, point); }

(8)编译、连接,运行后单击“用户界面线程”按钮,结果如图14.2所示。

图14.2 运行结果

上面介绍了创建用户界面线程的基本方法。对于用户界面线程来说,因为该线程拥有窗口,所以可以处理消息;而对于工作线程来说,除非自己加入消息处理机制,否则不会处理消息。下面介绍工作线程的创建办法。

【示例14.2】在对话框应用程序中创建工作线程。具体的操作步骤如下: (1)创建一个基于 MFC 对话框的应用程序 test1。

(2)在对话框上部添加一个按钮控件,其 ID 为 ID_START,标题为“开始工作线程”,再添加一个进度控制条控件,并且利用类向导添加一个变量 CProgressCtrl m_progress。

(3)在 TestDlg.cpp 中添加结构体如下: struct threadInfo{

int i;

CProgressCtrl *pctrlProgress; // 进度条对象指针 }Info;

上面的结构体中,变量 i 为进度条的速度,pctrlProgress 为进度条的指针。 (4)编写一个函数 fun(),利用该函数实现进度条的步进。具体代码如下: UINT fun(LPVOID p){

threadInfo *pInfo=(threadInfo *) p; // 线程函数的参数 for(int i=0; i<100; i++){

int nTemp=pInfo->i;

pInfo->pctrlProgress->SetPos(i); // 设置进度条步进 Sleep(nTemp); }

return 0; }

上面的代码是利用参数设置进度条的进度。

(5)添加单击“开始工作线程”按钮的消息映射(右击该按钮,选择 Events„->Add Handler—OnStart->Edit Existing),编写函数体,让创建工作线程调用上面函数。代码如下:

void CTestDlg::OnStart(){

// TODO: Add your control notification handler code here Info.i=9;

Info.pctrlProgress=&m_progress;

CWinThread *pThread=AfxBeginThread(fun, &Info); // 其它线程 }

上面的代码必须放在 fun() 函数的后面,或者在该函数的前面添加 fun() 函数的声明语句,否则会提示 fun() 函数没有定义。

(6)编译、连接,运行结果如图14.3所示。

本节介绍了线程创建的基本方法。同时介绍了工作线程和用户界面线程创建的区别。在下一节中给

出线程通信和线程同步的应用实例。

图14.3 运行结果

14.6 本章实例---多线程

前节介绍了创建用户界面线程和工作线程的具体实例,本节结合线程同步和线程通信给出综合实例。 【示例14.3】利用多线程技术编写一个对话框应用程序,在它包含的三个编辑框内同步输出 A、B、C 三个字母,每种字母输出 10 次。具体操作步骤如下:

(1)创建一个基于对话框的应用程序,如 test2。

(2)在对话框上添加三个编辑框控件,其 ID 分别为 IDC_EDIT1、IDC_EDIT2 和 IDC_EDIT3。再添加一个按钮控件,其 ID 为 ID_SHOW,称题为“显示”。

(3)利用类向导为三个编辑框添加变量,分别为 CEdit m_A、CEdit m_B、CEdit m_C。 (4)在TestDlg.cpp 中添加变量作为全局变量:

CSemaphore semaphoreWrite(3, 3); // 信号量对象 char g_Array[10]; // 字符数组

semaphoreWrite 是 CSemaphore 的对象,在第14.4节线程同步中曾介绍过这个类。本例中需要一个计数器来限制使用某资源的线程数目,故使用了信号量对象,并且将其初始化为 3。g_Array[10] 是保存输出字符序列的数组。

(5)在 TestDlg.cpp 中添加三个线程函数,以实现代码的输出。 UINT WriteA(LPVOID pParam){ // 线程函数

CEdit *pEdit=(CEdit*)pParam;

pEdit->SetWindowText(\"\"); // 清除编辑框中的文字

WaitForSingleObject(semaphoreWrite.m_hObject, INFINITE); // 等待信号量 CString str;

for(int i=0; i<10; i++){ // 逐个输出字母A

pEdit->GetWindowText(str); // 获取编辑框中的文字 g_Array[i]='A';

str=str+g_Array[i]; // 连接上一个字母A

pEdit->SetWindowText(str); // 在编辑框中输出字符 Sleep(1000); }

ReleaseSemaphore(semaphoreWrite.m_hObject, 1, NULL); // 退出临界区释放资源 return 0; }

UINT WriteB(LPVOID pParam){

CEdit *pEdìt=(CEdit*) pParam;

pEdìt->SetWindowText(\"\"); // 从清空窗口内容

WaitForSingleObject(semaphoreWrite.m_hObject, INFINITE); CString str;

for(int i=0; i<10; i++){

pEdit->GetWindowText(str); // 从窗口内取出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); CString str;

for(int i=0; i<10; i++){

pEdit->GetWindowText(str); // 从窗口内取出str内容 g_Array[i]='C'; str=str+g_Array[i];

pEdit->SetWindowText(str); Sleep (1000); }

ReleaseSemaphore(semaphoreWrite.m_hObject, 1, NULL); return 0; }

(6)添加单击“显示”按钮的消息映射,实现创建线程,进行输出,具体如下: void CTestDlg::OnShow(){

// TODO: Add your control notification handler code here // 线程1

CWinThread *pWriteA=AfxBeginThread(WriteA,

&m_A,

THREAD_PRIORITY_NORMAL, 0,

CREATE_SUSPENDED); pWriteA->ResumeThread(); // 线程2

CWinThread *pWriteB=AfxBeginThread(WriteB,

&m_B,

THREAD_PRIORITY_NORMAL, 0,

CREATE_SUSPENDED); pWriteB->ResumeThread(); // 线程3

CWinThread *pWriteC=AfxBeginThread(WriteC,

&m_C,

THREAD_PRIORITY_NORMAL, 0,

CREATE_SUSPENDED); pWriteC->ResumeThread(); }

上述代码利用 AfxBeginThread() 函数创建多个线程来实现输出,并使用 ResumeThread() 函数终止线程。

注意:需要包含头文件 #include \"afxmt.h\" 将 CSemaphore 类添加进来,否则会提示错误。同时上面的函数需放在步骤(5)中函数的后面,否则提示函数没有定义,当然,也可以在上述函数之前先声明步骤(5)中的三个函数,然后再调用它们。

使用 WaitForSingleObject() 函数还需要头文件 #include \"Window.h\"。 (7) 编译、连接,运行后单击“显示”按钮,结果如图14.4所示。

图14.4 运行结果

三个编辑框中同时逐个地输出字母,输出10个后停止。

【示例14.4】编写一个多线程的例子,它在一个对话框上使用4个进度条控件,利用线程同步让其中第1个和第2个进度条控件从0走到末尾后,再启动第3个进度条。第3个进度条结束后,第1个进度条再从头开始走。

分析:上面的叙述很明显是要求实现线程同步的例子。使用5个线程分别控制五5个进度条的步进过程。利用 CSemaphore 对象控制前两个线程,它们执行结束后才开始后面线程的执行,即只允许两个线程同时运行。第3个利用 CCriticalSection 对象控制它们走完后,才开始第4个进程运行。第4个利用 CEvent 对象控制执行结束后,再开始第1个运行。下面介绍具体步骤:

(1)创建一个基于对话框的应用程序,如 test3。

(2)在对话框上添加四个进度条控件,利用类向导分别添加四个 CProgressCtrl 变量 m_pro1、m_pro2、m_pro3、m_pro4。

(3)再定义3个全局变量,即类的对象: CCriticalSection section; // 临界区对象

CSemaphore semaphore(2, 2); // 访问资源的线程数最多2个,当前可访问线程数2个 CEvent event; // 事件对象

注意:上面的变量需要定义在 TestDlg.cpp 中,因为此文件包含了头文件 #include \"afxmt.h\"。 (4)声明5个线程函数,如下所示。

UINT write1(LPVOID pParam); // 函数原型 UINT write2(LPVOID pParam); // 函数原型 UINT write3(LPVOID pParam); // 函数原型 UINT write4(LPVOID pParar) ; // 函数原型 UINT write5(LPVOID pParam); // 函数原型 并且在 testDlg.cpp 中实现该函数,代码如下: UINT write1(LPVOID pParam){

WaitForSingleObject(semaphore.m_hObject, INFINITE); // 等待信号量 CProgressCtrl *p=(CProgressCtrl*)pParam; for(int i=0; i<100; i++){

p->SetPos(i); // 设置进度条步进 Sleep(100); }

ReleaseSemaphore(semaphore.m_hObject, 1, NULL); // 释放信号量 return 0; }

UINT write2(LPVOID pParam){

WaitForSingleObject(semaphore.m_hObject, INFINITE); // 等待信号量 CProgressCtrl *p=(CProgressCtrl*)pParam; for(int i=0; i<100; i++){

p->SetPos(i);

Sleep (100); }

ReleaseSemaphore (semaphore.m_hObject, 1, NULL); // 释放信号量 return 0; }

UINT write3(LPVOID pParam){

WaitForSingleObject(semaphore.m_hObject, INFINITE); // 等待信号量 section.Lock(); // 获取临界区对象 CProgressCtrl *p=(CProgressCtrl*)pParam; for(int i=0; i<100; i++){

p->SetPos(i); Sleep (100); }

section.Unlock(); // 释放临界区对象

ReleaseSemaphore(semaphore.m_hObject, 1, NULL); // 释放信号量 return 0; }

UINT write4(LPVOID pParam){

WaitForSingleObject(semaphore.m_hObject, INFINITE); // 等待信号量 section.Lock(); // 获取临界区对象 CProgressCtrl *p=(CProgressCtrl*)pParam; for(int i=0; i<100; i++){

p->SetPos(i); Sleep (100); }

section.Unlock(); // 释放临界区对象

ReleaseSemaphore (semaphore.m_hObject, 1, NULL); // 释放信号量 return 0; }

上述代码就是利用所定义的全局对象 section 实现线程的同步。具体代码的含义在此不再叙述,读者可参阅上面介绍的各同步类的具体含义。

(5) 添加一个按组控件(名为“启动”),右击它、并利用类向导添加单击该按钮的消息映射,代码如下:

void CTestDlg::OnStart(){

// TODO: Add your control notification handler code here CWinThread *pwrite1=AfxBeginThread(write1, // 创建线程 &m_pro1,

THREAD_PRIORITY_NORMAL, 0,

CREATE_SUSPENDED); pwrite1->ResumeThread(); ………

}

上述代码就是创建和启动线程函数的代码,其中省略了其它4个线程的定义,它们形式是完全相同的。 (6) 辑译、连接、运行后,单击“启动”按钮,运行结果如图14.5、图14.6和图14.7所示。

图14.5 前两个进度条移动 图14.6 第三个结束后第四个开始 图14.7 第四个结束,第一个开始

上面介绍了多线程中同步控制的基本方法。在实际应用中,多线程的同步控制是非常重要的要求,读者应深入理解同步的概念和实现方法。

14.7 上机实践

在对话框应用程序中添加两个进度条和一个按钮,然后单击按钮创建两个线程:一个线程在上面的进度条中连续显示进度,输出完成后,另一个线程在下面的进度条中显示进度。运行结果如图14.8 所示。

图14.8 运行结果

要求采用事件类进行控制。具体步骤如下:

(1)创建基于对话框的应用程序,如 test4,添加两个进度条控件,并添加变量 m_pro1 和m_pro2。

(2)定义 CEvent 类的对象,并声明两个全局函数。 CEvent event;

UINT write1(LPVOID pParam); UINT write2(LPVOID pParam); (3)实现这两个函数,控制进度条移动。

UINT write1(LPVOID pParam){

CProgressCtrl *p= (CprogressCtrl*)pParam; for(int i=0; i<100; i++){

p->SetPos(i) ; Sleep(100); }

event.SetEvent(); return 0; }

UINT write2(LPVOID pParam){

WaitForSingleObject(event.m_hObject, INFINITE); CProgressCtrl *p= (CprogressCtrl*)pParam; for(int i=0; i<100; i++){

p->SetPos(i) ; Sleep(100); }

event.SetEvent(); return 0; }

(4) 添加单击按钮的消息映射,启动线程: void CTestDlg::OnStart(){

// TODO: Add your control notification handler code here CWinThread *pwrite1=AfxBeginThread(write1, &m_pro1,

THREAD_PRIORITY_NORMAL, 0,

CREATE_SUSPENDED); pwrite1->ResumeThread();

CWinThread *pwrite2=AfxBeginThread(write2, &m_pro2,

THREAD_PRIORITY_NORMAL, 0,

CREATE_SUSPENDED); pwrite2->ResumeThread(): }

14.8 常见问题及解答

1、在控制台应用程序中能不能使用多线程,为什么?

答:在控制台应用程序中不能使用多线程,这是因为 Visual C++ 编译环境默认的设置在控制台应用程序中是单线程的。可以在 project setting|c/c++ 选项下,选择 code generation 标签,在 use run-time library 下拉列表中选择 debug multithread 选项即可。

2、进程与线和的关系是什么?

答:线程是可执行的代码段,而进程则只是一个循环而已。进程是不可以执行的,它只是为各个线程的执行提供所需的资源共享。 进程的产生必然会产生至少一个线程(即主线程)。永远不会出现没有线程的进程。

3、用户界面线程与工作线程的区别是什么?

答:用户界面线程通常是和一个窗体相联系的,这个线程拥有一个消息循环,可以对自己的消息进行处理。而工作线程通常是对后台数据的处理、统计等工作,它一般不需要与用户进行交互。不会得到消息循环,不可以对用户的操作进行响应。

14.9 小结

本章介绍了多线程程序设计的基本知识,多线程是提高程序运行效率的一种有效手段。本章对 MFC 支持的两种多线程:用户界面线程和工作线程分别进行了介绍,并通过实例讲解了线程通信和同步的基本设计步骤。通过本章的学习,读者可以掌握在 Visual C++ 中进行多线程程序设计的基本方法和设计步骤。[完]

因篇幅问题不能全部显示,请点此查看更多更全内容