多线程应用环境和线程对象模型

上传人:ji****72 文档编号:45838462 上传时间:2018-06-19 格式:PDF 页数:43 大小:496.16KB
返回 下载 相关 举报
多线程应用环境和线程对象模型_第1页
第1页 / 共43页
多线程应用环境和线程对象模型_第2页
第2页 / 共43页
多线程应用环境和线程对象模型_第3页
第3页 / 共43页
多线程应用环境和线程对象模型_第4页
第4页 / 共43页
多线程应用环境和线程对象模型_第5页
第5页 / 共43页
点击查看更多>>
资源描述

《多线程应用环境和线程对象模型》由会员分享,可在线阅读,更多相关《多线程应用环境和线程对象模型(43页珍藏版)》请在金锄头文库上搜索。

1、多线程应用环境和线程对象模型多线程应用环境和线程对象模型本文阐述多线程应用程序如何利用面向对象方法来解决线程安全性问题,并给出 了针对两种类型的同步对象如何进行面向对象封装的例子, 最后在对 CWinThread 类进 行简单分析后给出一个完全封装版本的 Thread 模型。18.118.118.118.1 引言引言引言引言当下的信息技术已经进入网络时代和分布式计算时代,也是多核时代和并行处理 时代!操作系统是多任务和多线程的,数据库是多任务和多线程的,服务器是多任务 的和多线程的,浏览器也是多任务和多线程的,一切都将是多任务和多线程的。这是 一个多任务和多线程的世界! 一提到进程和线程,人们

2、往往首先会想到 CreateProcess()、TerminateProcess()、 CreateThread()、ExitThread()、TerminateThread()、SuspendThread()和 ResumeThread() 等这样的 Win32 API 函数, 或者 pthread_create()、 pthread_exit()、 pthread_kill()等 POSIX 线程库函数,甚至是 C 库函数_beginthread()、_endthread()及其扩展版本和 MFC 库函 数 AfxBeginThread(),等等。这些 API 函数为多进程和多线程应用程序

3、的编写提供了 基本的线程控制手段, 但是都是面向过程的。 面向过程的应用程序本身是非模块化的, 到处充斥着全局变量和全局函数,十分难于管理和维护;如果是面向过程的多线程应 用程序,那么加上各个线程函数之间可能的共享全局变量和各种共享资源,就会使管 理和维护的难度成倍地增加。 其实多任务程序(多进程和多线程)的概念、优点和开发方法已经有很多资料和 书籍都讲述过了(网上可以搜出一大堆) 。通过阅读这些书籍、资料和文章,我们已经 对进程和线程建立起了如下基本的认识:现代的多任务操作系统允许同时运行多个应用程序也就是多个进程; 现代的多任务操作系统也允许一个进程创建多个子进程; 现代的多任务操作系统允

4、许一个进程创建多个线程并可在同一时刻执行这些 线程;进程启动时实际上是由操作系统创建了一个进程数据结构并接着创建一个线 程后开始执行的,这个线程就是主线程,然后可以在任何线程中再创建其他 线程;进程是一个管理多个线程、多个模块(动态链接库和子进程)以及各种资源 的大型数据结构, 而不是真正的执行体。 线程才是操作系统分配 CPU 时间的 基本执行单位(基本调度对象) ;进程管理的资源包括虚拟地址空间、代码、各种文件和数据、动态分配来的 内存、端口、句柄、环境变量、设备和命令行参数等等,进程内的各个线程 可以共享这些资源;每个线程都有自己独立的一套上下文环境(即一组寄存器状态和优先级)和 独立的

5、堆栈,操作系统进行线程切换时会用到它们;进程中的多个线程可以执行该进程中相同的代码片段 (同时或不同时) , 也就 是代码重入;2操作系统调度线程就是用一个硬件定时器和一套复杂的游戏规则(优先级管 理)把 CPU 时间轮流分配给各个线程(即上下文切换) ,以使各个线程都有 机会在 CPU(单个或多个)上得到执行。通常 CPU 时间片的长度为几十个 毫秒; 线程切换会发生在同一个进程内的两个线程间,也会发生在不同进程的两个 线程间。如果是后者,就会先进行两个进程的上下文切换,然后再在其中进 行线程的上下文切换; 现代的抢占式多任务操作系统不会保证两个线程的执行顺序,我们也无法预 测两个线程的执行

6、顺序,所以我们绝对不能假设两个线程的执行顺序。这正 是产生竞争条件的原因,也是需要使用同步机制的理由; 在单处理器平台上, 操作系统只能轮流调度各个线程, 由于线程切换非常快, 所以就给用户造成“多个线程并行执行” 的假象,这是宏观上的并行处理; 在多处理器平台上,支持 SMP(对称多处理)技术的操作系统就可以让各个 线程在单独的处理器上同时执行甚至直到线程结束,可以同时执行的线程数 等于处理器的个数, 这是真正的并行处理; 而不支持 SMP 技术的操作系统无 法发挥多处理器的并行优势; 多进程和多线程技术主要的目的是充分发挥硬件和操作系统的并行工作能 力,以提升软件的性能和用户体验; 多线程

7、技术是通过函数库或操作系统 API 的形式支持的,而不是编程语言直 接支持的。我在上面提到了“用户体验”这个词,现在很多领域都在使用它,好像很时髦的 样子。我见的最多的就是 Microsoft 动辄问你是否参与它们的“客户体验改善计划” 。 呵呵! 用户体验其实就是用户使用某种产品时的感觉或者满意度。有用没有用?好用不 好用?舒适不舒适?方便不方便?友好不友好?好看不好看?这都是用户体验涉 及的内容。 我为什么说多进程和多线程技术可以提升用户体验呢?这主要是指软件对用户操 作的反应能力。因为在单处理器平台上不可能有真正的多任务,所以多线程程序并不 会让程序执行得比它的单线程版本来得更快,但是至

8、少可以保证 UI 线程不会被阻塞 (UI 消息用一个专门的线程处理) 。如果一个软件经常需要做大量的计算工作比如视 音频编解码,同时还要积极响应用户通过 UI 进行的修改参数操作,而如果把计算工 作也放在 UI 线程里的话,就会导致 UI 反应迟缓(如对鼠标移动、点击和键盘操作反 应迟缓) ,甚至出现假死现象。为了解决这个问题,我们可以把计算工作分离出来单独 放在另一个线程中,而让 UI 线程专职处理人机交互事务,这样 UI 基本不受影响。特 别是有些任务具有严格的时间限制,比如要求“每隔一段固定的时间就执行一个固定 的操作” , 像这样的问题就必须放在一个单独的线程或定时器中完成, 以保证其

9、他工作 不会影响这个任务的周期。 通常,我们把主线程作为 UI 线程(即会启动 UI 消息循环) ,而且仅设置一个 UI 线程,而把其他任务交给单独的子线程来完成,这些子线程就称为 worker 线程(不启 动 UI 消息循环) 。 本文不打算讲述多进程开发,也不再重复线程的基本知识和细节,而是将主要精 力集中在多线程应用程序开发过程中必然碰到的一些问题、解决这些问题的技术以及 工具。318.218.218.218.2 多线程应用程序需要面对的主要问题多线程应用程序需要面对的主要问题多线程应用程序需要面对的主要问题多线程应用程序需要面对的主要问题多线程技术为我们带来了应用程序性能的提升,但同时

10、也带来了一些风险,比如 可能编写出脆弱的程序,甚至会碰到程序意外崩溃等问题,而且多线程应用程序很难 调试。凡开发过多线程应用程序的读者一定对此深有体会。 当在进程中创建多个线程来协同工作时,我们往往需要考虑下列问题: 谁可以创建线程?在什么时刻需要创建新的线程? 总共有多少个线程?需要多少个线程同时运行? 每个线程完成什么工作任务?它们的生命周期有多长? 哪些线程最先启动,哪些线程最后结束? 线程执行过程中如何处理失败和异常? 哪些线程之间需要同步?如何同步才是安全的? 是否只要在线程之间有共享数据,就一定需要同步?什么情况下才需要同 步? 哪些线程之间需要通信?如何通信才是安全的? 如何安全

11、地结束一个线程?其实这些问题就是我们常说的多线程规划、设计和线程安全性问题。线程安全性 问题归结起来就是如何避免死锁、无限延迟和数据竞争(或者叫资源竞争、竞争条件) 等情况的发生,它们是多线程应用程序开发中需要重点对付的三座大山!18.2.118.2.118.2.118.2.1 死锁死锁如果两个线程互相等待对方释放某个资源,或者互相等待对方先结束,然后各自 才能继续,那么就会出现死锁。死锁分为互锁和自锁,上面描述的就是互锁互相 锁死,如图 18-1 所示:线 程T1线 程T2资 源1014时 间23资 源2获 得 资 源1获 得 资 源2等 待T1释 放 资 源1等 待T2释 放 资 源2释

12、放 资 源2释 放 资 源1图 18-1 共享资源可能导致死锁这里的资源可能是排他性共享锁(如互斥量)本身,也可能是由排他性共享锁保护起 来的某种资源。这种情况与线程的启动时机有很大关系,所以这种死锁不是必然会发 生的,而是潜在的、随机发生的。因为对于共享锁这种同步对象,一个线程总是能够 成功获得它,除非有另一个线程已经占有它并且还没有释放。4另一种情况是两个线程互相等待某个事件的发生,并且又都需要在稍后触发对方 等待的事件,如图 18-2 所示:线 程T1线 程T2事 件1事 件2等 待 事 件1发 生 等 待 事 件2发 生触 发 事 件1触 发 事 件2图 18-2 互相等待对方的事件导

13、致死锁在这种情况下,如果又没有第三者来触发其中任何一个事件,就会发生死锁。这种情 况与两个线程启动的时机没有关系,所以是必然会发生的。 自锁是互锁的一种特殊情况:一个线程 T 先锁定了某项资源,而在它自己或者别 的线程释放该资源之前又试图再次锁定该资源。例如下面的示例代码就说明了这种情 况:void foo()static Mutex guard;guard.Lock();/ 第一次加锁,成功!/ 访问共享资源 Rguard.Lock();/ 释放之前再次加锁,可能阻塞!/ 访问共享资源 Rguard.UnLock();guard.UnLock();foo()的调用者线程可能会被阻塞, 而此时

14、又没有其他线程可以调用 guard.UnLock() 来解锁,所以就发生了自锁。 自锁常常发生在函数调用之间,往往是由于程序员疏忽大意造成的。比如两个物 理上相隔很远的函数共享同一个互斥量,而程序员却忘记了这一点。示例如下:/ in foo.cppMutex g_Guard;void foo()g_Guard.Lock();/ 加锁/ 访问共享资源 Rg_Guard.UnLock();/ 解锁/ in bar.cppextern Mutex g_Guard;5extern void foo();void bar()g_Guard.Lock();/ 加锁/ 访问共享资源 Rfoo();/ 调用

15、foo()g_Guard.UnLock();/ 解锁大家可以看出来,自锁其实就是一个函数或者一个线程在执行过程中嵌套或者连 续锁定同一个互斥量(中间没有解锁操作) 。对于这种情况,Win32 内核库对互斥量和 临界区的实现以及其他平台对 POSIX 线程库的实现都允许嵌套锁(甚至可以多次锁 定) , 并且保证除第一次锁定外后面连续的锁定都不会阻塞调用线程自己 (也即一旦该 线程第一次加锁成功,则后续的加锁都会立即成功而不会被阻塞) 。针对这种特点, 如 果用户运用不当可能就会导致潜在的失步,而且很难发现。特别是如果用户将配对的 Lock()和 UnLock()操作分别放在两个不同的函数中的时候

16、, 这种潜在的风险就更大了, 因为这两个函数很可能分别被两个不同的线程调用,而调用的顺序却是无法预期的、 不保证的。例如:/ in foo.cppMutex g_Guard;void foo()/ 其他代码g_Guard.Lock();/ (1) 加锁/ 访问某共享资源 R / 没有释放锁/ in bar.cppextern Mutex g_Guard;void bar()/ 访问同一个共享资源 Rg_Guard.UnLock();/ (2) 在这里释放锁/ 其他代码如果有一个线程 A, 它在执行过程中循环地调用函数 foo(), 每次调用 foo()以后立 即创建一个子线程 B, 而子线程 B 在启动后即调用函数 bar()。 线程 A 可能期望子线程 B 能够在线程 A 下次调用 foo()之前就执行到语句(2)g_Guard.UnLock()来解锁, 但是事 实上这种期望可能会落空。 因为子线程 B 到底是否会先于父线程 A 执行

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 行业资料 > 其它行业文档

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号