第9章 多 线 程
本章主要介绍Windows操作系统下多线程的基本概念、创建管理线程的方法,以及线程的同步问题。
同时,MFC对线程操作进行了封装,提供了支持线程操作的类库。 本章主要讨论这些技术,并且给出相应的实例。
本章主要内容如下: ● Windows下多线程的基本概念; ● 用户界面线程和工作者线程; ● 线程的管理操作; ● 线程的同步; ● 多线程编程实例。
多线程的基本概念 两种重要的线程 线程的操作 9.1 两种重要的线程 9.2 线程的操作 9.3 小 结 9.4
9.1 多线程的基本概念 在Win32下,一个应用程序由一个或多个进程组成。 9.1 多线程的基本概念 在Win32下,一个应用程序由一个或多个进程组成。 一个进程由一个或多个线程以及代码、数据和其他内存中的程序资源组成。 典型的程序资源包括打开的文件、信号量、动态分配的内存等。 线程在进程空间中执行。
线程是操作系统分配处理器时间的最小单位。 每个线程有自己的堆栈、CPU寄存器、以及程序入口。 每个线程共享所有处理器的资源。
进程中的每个线程都独立执行,不会影响该进程中的其他线程。 所有线程共享公共的资源。 因此必须采用信号量或者其他进程间通信方法来调整线程的工作。
9.2 两种重要的线程 Windows提供了两种线程,即用户界面线程和工作者线程。 9.2 两种重要的线程 Windows提供了两种线程,即用户界面线程和工作者线程。 用户界面线程通常用来处理消息循环、与用户交互,工作者线程用来处理后台的计算。 下面分别介绍这两种线程。
9.2.1 用户界面线程 每一个Windows应用程序都有一个主线程。 这里的用户界面线程是指用来和用户进行交互的线程。
接收用户传送的数据,并且做出响应。 用户界面线程通常包含自己的窗口,有自己的消息循环,独立于应用程序的其他部分。
创建一个用户界面线程需要首先继承线程类CWinThread,重载它的成员函数,如表9-1所示。 最后调用AfxBeginThread创建线程对象。
表9-1 需要重载的CWinThread的成员函数 函 数 名 称 作 用 InitInstance 线程的初始化,通常需要重载 ExitInstance 释放线程占用的资源,通常需要重载 OnIdle 空闲时间的处理,不一定重载 PreTranslateMessage 过滤消息,不一定重载 ProcessWndProcException 处理线程抛出的意外 Run 线程控制函数,通常不重载
9.2.2 工作者线程 工作者线程通常用来处理后台运行的任务。 在后台任务运行的同时,用户可进行其他操作,不必等待后台任务的结束。
例如,一个三维模型编辑软件,用户要对两个模型做布尔运算。 在进行计算的同时,用户希望可以观察两个模型,对模型进行旋转,缩放的操作。
再如文本浏览软件的打印功能,在打印文本的工程中,用户仍然会继续浏览文本内容。 这些都属于工作者线程。
创建一个工作者线程只需要两个步骤。 首先实现工作者线程的功能函数,然后启动线程即可。
可以调用Win32提供的API函数CreateThread创建一个线程,MFC对Win32的线程操作做了封装,也可以通过调用AfxBeginThread创建一个线程对象。 这些函数及其调用方法将在下一节详细介绍。
9.3 线程的操作 本节介绍Windows线程的操作方法,包括线程的创建、线程的管理、线程的同步、线程的终止等。
9.3.1 线程的创建 线程的创建方法有3种,分别介绍如下。 9.3.1 线程的创建 线程的创建方法有3种,分别介绍如下。 1.调用Win32API函数CreateThread和CreateRemoteThread (1)CreateThread (2)CreateRemoteThread
2.C运行库函数_beginthreadex 3.调用函数AfxBeginThread
2.Win32API函数TerminateThread 3.C运行库函数 4.函数AfxEndThread 9.3.2 线程的终止 1.调用Win32API函数ExitThread 2.Win32API函数TerminateThread 3.C运行库函数 4.函数AfxEndThread
9.3.3 线程的管理和操作 1.线程的挂起、继续和休眠 (1) 挂起。 (2)继续。 (3)休眠。
2.线程的优先级 (1)级别。
表9-2 进程优先级列表 优 先 级 优 先 级 值 ABOVE_NORMAL_PRIORITY_CLASS 表9-2 进程优先级列表 优 先 级 优 先 级 值 ABOVE_NORMAL_PRIORITY_CLASS 大于NORMAL_PRIORITY_CLASS, 小于HIGH_PRIORITY_CLASS BELOW_NORMAL_PRIORITY_CLASS 大于IDLE_PRIORITY_CLASS, 小于NORMAL_PRIORITY_CLASS
续表 优 先 级 优 先 级 值 HIGH_PRIORITY_CLASS 13 IDLE_PRIORITY_CLASS 4 优 先 级 优 先 级 值 HIGH_PRIORITY_CLASS 13 IDLE_PRIORITY_CLASS 4 NORMAL_PRIORITY_CLASS 9(前台)或7(后台) REALTIME_PRIORITY_CLASS 24
表9-3 线程优先级列表 优 先 级 级 别 THREAD_PRIORITY_ABOVE_NORMAL 比进程优先级高一级 THREAD_PRIORITY_BELOW_NORMAL 比进程优先级低一级 THREAD_PRIORITY_HIGHEST 比进程优先级高两级 THREAD_PRIORITY_LOWEST 比进程优先级低两级 THREAD_PRIORITY_NORMAL 与进程优先级相同 THREAD_PRIORITY_TIME_CRITICAL 把线程优先级设为15 THREAD_PRIORITY_IDLE 把线程优先级设为1
(2)优先级的设置和获取。
3.线程ID的判断 4.线程的切换 5.打开线程 6.线程函数ThreadProc 7.获得线程的时间信息
8.处理器相关操作 (1)SetThreadAffinityMask函数 (2)SetThreadIdealProcessor函数
9.3.4 线程间的通信 线程间的通信通常采用共享全局变量,共享存储区来实现。 因为所有的线程都可以访问这些资源。 主线程不能通过发送消息给辅助线程实现通信,但辅助线程可以通过发送自定义消息达到和主线程通信的目的。
本节将通过一个简单的实例,介绍使用共享存储区和自定义消息实现线程间通信的功能。 【例9-1】 线程之间通信实例。
(a) (b) 图9-1 程序运行界面
9.3.5 线程的同步 在多线程程序设计中,经常会出现两个或多个线程使用一个公共变量,或者多个线程共享一些公共存储区的情况。
凡是涉及共享资源的情况都有可能会引起程序的错误。 为了解决这些问题,Windows提供了大量线程的同步方法,例如,变量锁、临界区、信号量、事件对象、互斥对象等。
一个或两个变量的互锁操作是最简单的同步原语。 Win32提供了7个具有线程安全性的原子操作,具体介绍如下。 1.互锁操作 一个或两个变量的互锁操作是最简单的同步原语。 Win32提供了7个具有线程安全性的原子操作,具体介绍如下。 (1)InterlockedIncrement (2)InterlockedDecrement
(3)InterlockedExchange (4)InterlockedExchangeAdd (5)InterlockedExchangePointer (6)InterlockedCompareExchange (7)InterlockedCompareExchange Pointer
临界区(Critical Section)是一段程序代码,在任何时候都只能被一个线程使用。 2.临界区 临界区(Critical Section)是一段程序代码,在任何时候都只能被一个线程使用。 如果有多个线程同时访问临界区,这时只能有一个线程进入,其他线程则等待,直到临界区被释放。
与其他同步方法不同的是,临界区只能在单个进程内使用。 使用临界区的时候要避免长时间锁住一份资源。
进入临界区后必须尽快地离开,释放资源。 如果是主线程(GUI线程)要进入一个没有被释放的临界区,将会出现错误。
(1)InitializeCriticalSection (2)DeleteCriticalSection (3)EnterCriticalSection (4)LeaveCriticalSection (5)CcriticalSection
表9-4 CEvent类的成员函数 函 数 名 称 作 用 CCriticalSection 作 用 CCriticalSection 构造函数,构造CCriticalSection对象 Lock 进入临界区 UnLock 离开临界区
事件(Event)是由Windows操作系统管理的同步对象,可以用于进程或线程的同步。 3.事件 事件(Event)是由Windows操作系统管理的同步对象,可以用于进程或线程的同步。 一个事件被创建后,只有激发状态和未激发状态两种状态,也称为发信号状态和未发信号状态。
事件包括手动重置事件和自动重置事件两种类型。 手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持激发状态,直到程序重新把它设置为未激发状态。
自动重置事件被设置为激发状态后,会唤醒一个等待中的线程,然后自动恢复为未激发状态。 所以用自动重置事件来同步两个线程比较理想。
(1)CreateEvent (2)OpenEvent (3)SetEvent、ResetEvent和PulseEvent函数 (4)Cevent
表9-5 CEvent类的成员函数 函 数 名 称 作 用 CEvent 构造函数,构造CEvent对象 SetEvent 作 用 CEvent 构造函数,构造CEvent对象 SetEvent 启动事件对象,释放等待线程 PulseEvent 启动事件对象,释放等待线程,或者重置事件对象 为未激活状态 ResetEvent 设置事件对象为未激活状态 Unlock 释放事件对象
4.互斥器 互斥器(Mutex)的功能与临界区相似。区别在于互斥器所花费的时间比临界区多很多,但是互斥器是核心对象(后面介绍的Event和Semaphore也是核心对象),可以跨进程使用,而且等待一个被锁住的互斥器可以设定TIMEOUT,不会像临界区那样无法得知临界区的情况,一直等待。
Win32提供了创建互斥器CreateMutex(),打开互斥器OpenMutex(),释放互斥器ReleaseMutex()等操作。
Mutex的拥有权并非属于产生它的那个线程,而是属于最后对此Mutex进行等待操作(Wait ForSingleObject)并且尚未进行ReleaseMutex()操作的线程。
线程拥有Mutex就好像进入临界区一样,一次只能有一个线程拥有该Mutex。 如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个Mutex就被舍弃了。
当其他线程等待这个Mutex时,仍能返回,并得到一个WAIT_ABANDONED_0返回值,一个Mutex被舍弃是Mutex特有的功能。
(1)CreateMutex (2)OpenMutex (3)ReleaseMutex (4)CMutex
信号量(Semaphore)是最具历史的同步机制。 信号量是解决producer/consumer问题的关键要素。 5.信号量 信号量(Semaphore)是最具历史的同步机制。 信号量是解决producer/consumer问题的关键要素。 对应的MFC类是CSemaphore。 Win32函数CreateSemaphore()用来产生信号量。
Release Semaphore()用来解除锁定。 Semaphore的现值代表的意义是目前可用的资源数,如果Semaphore的现值为1,表示还有一个锁定动作可以成功。
如果现值为5,就表示还有5个锁定动作可以成功。 当调用Wait等函数要求锁定,如果Semaphore现值不为0,Wait马上返回,资源数减1。
当调用ReleaseSemaphore()资源数加1,当时不会超过初始设定的资源总数。
(1)CreateSemaphore (2)OpenSemaphore (3)ReleaseSemaphore (4)Csemaphore
小 结 本章主要介绍Windows操作系统下多线程的基本概念,如何创建和管理线程,以及线程的同步问题。
通过本章的介绍,读者可以看出,多线程程序设计通常比一般的单线程程序复杂,在程序设计过程中,一定要考虑清楚各线程的关系,避免出现死锁或不同步的现象。
另外需要注意现在大多数用户使用单CPU计算机,在这种机器上运行多线程程序,有时反而会降低系统的性能。 因此,在设计多线程应用程序时,应慎重选择,视具体情况加以处理,使应用程序获得最佳的性能。