前言
在介绍多线程时,首先分析进程、以及多道程序设计模型。进程是操作系统中最重要的抽象概念之一,使得在即使只有一个CPU的机器上,也支持(伪)并发,即将一个单独的CPU变成多个虚拟的CPU。
多道程序设计
在进程执行过程中常常会因为资源请求或者IO被阻塞或中断(有的请求或者中断需要处理很长时间),此时CPU便空闲出来,众所周知,CPU是计算机中非常宝贵的资源,为提高其利用率,操作系统需通过进程切换,将CPU交给就绪队列的某个进程使用;等上次被阻塞或者中端的进程再次满足执行条件(一般是请求的资源得到满足或者IO完成等)后,操作系统便通过调度算法将CPU再次交给该进程执行任务,其中操作系统的进程调度算法有多种(例如在交互式系统(windows)中的进程调度算法有时间片轮转调度、优先级调度、多级队列调度等,这些相信大家都很熟悉了,此处不再进一步阐述),具体选择依赖操作系统。这时从用户的角度感觉是多个进程在同时执行,这的易于操作系统通过进程调度将一个cpu变成多个虚拟的CPU,实现多个进程的伪并发。在《现在操作系统》中将这种进程间的切换称之为“多道程序设计”。
进程切换:由于每个进程的处理任务异样性,其进程的输入、输出、处理过程、处理状态的都不同,那在进程切换的过程中是否也应该考虑这些参数呢?
答案是肯定的。一个正在执行的进程包括程序计数器、寄存器、变量的当前值等,而这些数据都是保存在CPU的寄存器中的,且这些寄存器只能是正在使用CPU的进程才能享用,所以在进程切换时,首先得保存上一个进程的这些数据(便于下次获得CPU的使用权时从上次的中断处开始继续顺序执行,而不是返回到进程开始,否则每次进程重新获得CPU时所处理的任务都是上一次的重复,可能永远也到不了进程的结束出,因为一个进程几乎不可能执行完所有任务后才释放CPU),然后将本次获得CPU的进程的这些数据装入CPU的寄存器从上次断点处继续执行剩下的任务。操作系统为了便于管理系统内部进程,为每个进程创建了一张进程表项,如表1所示。
进程管理 | 存储管理 | 文件管理 |
寄存器 | 正文段指针 | 根目录 |
程序计数器 | 数据段指针 | 工作目录 |
程序状态字 | 堆栈指针 | 文件描述符 |
堆栈指针 |
| 用户ID |
进程状态 |
| 组ID |
优先级 |
|
|
调度参数 |
|
|
进程ID |
|
|
父进程 |
|
|
进程组 |
|
|
信号 |
|
|
进程开始时间 |
|
|
使用CPU的时间 |
|
|
子进程的CPU时间 |
|
|
下次报警时间 |
|
|
多道程序设计的基本知识回顾差不多了,下慢我们来探讨下多道程序设计模型。
从上面的分析可以得知,多道程序设计可以提高cpu的利用率。但严格来讲,如果进程的计算平均时间是进程在内存中停留时间的20%,且内存中同时有5个进程,则CPU将一直处于满负荷状态,然而在模拟在实际中过于乐观,因为其假设这5个进程不会同时等待IO。
下面我们从概率的角度分析CPU的利用率,假设进程等待IO操作的时间与其停留内存的时间比例为p,当内存有n个进程时,则n个进程同时等待IO的概率为pn。则CPU的利用率为:
CPU的利用率 = 1 - pn
图1以n为变量的函数表示了CPU的利用率,n为多道程序设计的道数。
从图中可以看出当进程花80%进行IO时(IO密集型),需要大约10个进程并发才能使CPU得到充分利用;而当进程只花20%的时间进行IO时(称CPU密集型),孩子需要2个进程就可以使CPU的浪费率低于10%。在实际的应用中,不管一个等待用户从终端输入的交互式进程还是做大量读写磁盘的服务器进程80%甚至更多的IO时间是普遍的,所以通过多道程序设计模式可以提高IO密集型进程的CPU利用率,从而间接提高了整个系统的吞吐量;而对于CPU密集型进程,其并发度与CPU利用率不一定是成真比例。这套标准体系对与多线程同样适用,我们稍后进一步分析。
多道程序设计就回顾到这里,可见其核心就是通过进程调度提高CPU的利用率,将一个CPU虚拟成多个,实现多个进程的并发执行,至于进程如何创建、销毁、以及状态和状态转换、进程的层次结构以及进程的实现,此处就不阐述了,如果答不上或者不能随手捻来的话,还是回去看看《现代操作系统》这本书吧。
下面我们就一起谈谈线程的相关知识以及与进程的关系。当你读到这里,有的同鞋肯定会问,既然多道程序设计可以提高CPU的利用率,并实现多个进程的并发执行。如果你在提这个问题,表示你在思考,如果你还没有意识到这个问题,建议你停下结合之前学习的知识先想一想,看看是否自己能给出一个答案?
线程
我们先看看维基百科对线程的定义:线程(英语:thread)是操作系统能夠進行運算调度的最小單位。它被包含在进程之中,
是行程中的實際運作單位。一条线程指的是进程中一个单一顺序的控制流,一個进程中可以並行多個线程,每条线程并行执行不同
的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。此外,从资源分配的角度看,进程是资源所有资源分配的基本单位,线程则是CPU调度的基本单位,即使在单线程进程中也是如此。
引入线程的原因:
1) 一个应用程序中同时存在多个任务,其中的部分活动会随时间的推移而阻塞,而另外一部分则不会,例如,一个文字处理软件,前台部分需要从终端设备获得输入或者将处理完的部分输出,而后台线程则可以实现对文字的处理。故对于CPU密集型进程,该用多线程其性能不一定能得到很大提高,但对于IO密集型进程,其性能可得到很大提高。
2) 线程比进程更轻量级,创建和撤销的代价小,在许多系统中,创建一个线程比一个进程要快10~100倍不等。
3) 在多核CPU中,真正的并行有了可能。即在多线程设计中一部分可用来处理前台任务,一部分可用来处理后台任务,实现真正意义上的并行。
4) 线程间的切换代价要比进程切换的代价小。
引入多线程的原因:
1)某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程执行可以有效利用等待时间。如等待网络响应可能需要几秒的时间。
2)某个操作(常常是计算)会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多线程可以让一个线程负责交付,另一个线程负责计算。
3)多CPU或者多核计算机,本身具备同时执行多个线程的能力,故单线程无法完全发挥计算机的计算能力。
4)相对于多进程应用,多线程在数据共享方面效率要高很多。
5)程序逻辑本身就要求并发操作。
现在我们通过考察一个例子,就可以更清楚看出多线程的有益之处了。
假设用户正在编辑一本书。对于编辑这来说,最容易的办法是把正本书作为一个文件,便于编辑;而对于计算机来说把每个章节作为一个文件处理起来更快,但对于编辑者来说修改就太麻烦了,因为有的修改不止设计一个章节而是整本书,例如在整本书中替换某个词或字等等,如果整本书作为一个文件,正样处理就方便多了。否则,就得对每个章节所在文件进行处理。
现在如果用在一个1000页的文档中删除第一页的某一行的某个词,为保证格式的正确性,字处理软件需要对文档进行格式处理。但此时用户需要立刻跳到地800也进行另外一处修改,于是字处理软件被强制对整个书的前800页进程格式处理,因为在排列该页前面的所有页面之前,字处理软件并不知道第800页的第一行应该在哪里。而在第800也的页面可以显示在屏幕之前,计算机可能要拖延想当长一段时间进行处理,从而令用户不甚满意。
此时,多线程便可以有用武之地了。假设字处理软件编写成含有两个线程的程序。一个线程处理用户的交互,另一个用来在后台进行格式处理。一旦第一页发生的修改,交互线程就立即同时后台格式处理线程重现整理整本书的格式。同时,交互式线程继续监控用户的鼠标、键盘,并响应诸如第一页之类的简单命令,此刻,后台线程正在进行疯狂的运算,如果运气好的话,格式整理可能在用户请求查看第800页之前完成,这样用户就感觉不到延迟了。
同理,为保证用户的编辑工作得到及时保存,可以在添加一个现场周期性对文件进现场可以处理磁盘备份,而不必干扰其他两个线程。拥有三个线程的情形如图2所示。
图2 三个线程的字处理软件
试想,如果是单线程的话,那么在进行磁盘备份的时,来自键盘或者鼠标的命令就会被忽略,直至备份完成。有的同鞋会说,可以引入中断机制来中止备份操作,相应鼠标和键盘的命令,但其复杂性可想而知。如果引入三个线程,其设计就简单多了,一个线程用于与用户交互,第二线程在得到地一个线程的通知后在后台进行文档的格式化处理,第三个线程则周期性将ARM内的内容被封到磁盘。
此处,很显然,这里用三个不同的进程是不能工作的,因为三个线程都需要在同一个文件上进行操作,通过三个线程,由于一个进程内的所有线程共享公共内存,于是便可以在同一文件上进行处理。同理,其他的交互式程序也可以采用同样的设计方法。
看完这个例子,部分对多道程序设计比较忠心的同学可能会问,上述三个线程能协同完成工作,主要的便利之处在于其共享了进程中的公共内存空间。同样,也可以采用进程通信的方式来协同完成工作?
答案确实如此,但仔细思考几个问题:
1、进程通信与线程通信的代价孰高孰第?
2、进程切换和线程切换的代价?
3、如何保证三个进程所处理内容的一致性?而多线程方案中由于都是对同一文档内容进行处理,其一致性的保证则简单很多。
如果能准确解答上述几个答案,为什么不选择多道程序设计方案来完成其上述工作的原因就不攻自破了吧。
线程的内容我们回顾差不多了,现在我们来一起看看进程与线程的关系和区别吧。
进程与线程:
进程是操作系统的管理单位,而线程则是进程的管理单位;一个线程至少包含一个执行线程。不管是在单线程还是多线程中,每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但为返回的过程)。虽然线程寄生在进程中,但与他的进程是不同的概念,并且可以分别处理:进程是系统分配资源的基本单位,线程时调度CPU的基本单位。
多线程是对多进程的模拟。前者,多个线程共享同一个地址空间和其他资源,后者共享物理内存、磁盘、IO等其他资源,故线程被称为“轻量级进程”。多线程在但CPU系统中运行时,线程轮流运行,犹如多道程序设计,制造线程并行运行的假象。在一个有三个CPU密集型的进程中,实际上每个线程在一个CPU上得到的真实CPU速度的三分之一。不过随着技术的发展,目前主流的CPU都已经直接硬件支持多线程,并允许线程在几个纳秒级内完成切换。后续会对多进程、多线程、以及多核之间的关系进行总结。
线程间不像进程之间那样存在很大的独立性,一个进程的多个线程共享进程内部的很多资源,线程间可以互写对方的堆栈,而不同的进程则无法对其他进程的地址空间进行写操作。因此,在实现多线程编程中,应设计合理的同步通信机制,避免数据冲突的现象发生。图3给出了进程、线程的内容,其中进程的内容是该进程的所有线程共享的。
每个进程中的内容 | 每个线程中的内容 |
地址空间 | 程序计数器 |
全局变量 | 寄存器 |
打开文件 | 堆栈 |
子进程 | 状态 |
即将发生的报警 |
|
信号与信号处理程序 |
|
账户信号 |
|
同步、互斥信号量 |
|
本节就先写到这里,这要阐述了多进程、多线程。
转载自:http://blog.csdn.net/jomie/article/details/24878643