很难做到跨平台移植,以及对于多线程模型的透明。用户级线程模型的优势是线程切换效率高,因为它不涉及系统内核模式和用户模式之间的切换;另一个好处是应用程序可以采用适合自己特点的线程选择算法,可以根据应用程序的逻辑来定义线程的优先级,当线程数量很大时,这一优势尤为明显。但是,这同样会增加应用程序代码的复杂性。有一些软件包(如POSIXThreads 或Pthreads 库)可以减轻程序员的负担。
内核级线程往往指操作系统提供的线程语义,由于操作系统对指令流有完全的控制能力,甚至可以通过硬件中断来强迫一个进程或线程暂停执行,以便把处理器时间移交给其他的进程或线程,所以,内核级线程有可能应用各种算法来分配处理器时间。线程可以有优先级,高优先级的线程被优先执行,它们可以抢占正在执行的低优先级线程。在支持线程语义的操作系统中,处理器的时间通常是按线程而非进程来分配,因此,系统有必要维护一个全局的线程表,在线程表中记录每个线程的寄存器、状态以及其他一些信息。然后,系统在适当的时候挂起一个正在执行的线程,选择一个新的线程在当前处理器上继续执行。这里“适当的时候”可以有多种可能,比如:
- 当一个线程执行某些系统调用时,例如像sleep 这样的放弃执行权的系统函数,
- 或者像wait 或select 这样的阻塞函数;
- 硬中断(interrupt)或异常(exception);
- 线程终止时,
等等。由于这些时间点的执行代码可能分布在操作系统的不同位置,所以,在现代操作系统中,线程调度(thread scheduling)往往比较复杂,其代码通常分布在内核模块的各处。在本章后面,我们将会看到Windows中的线程调度是如何实现的。
内核级线程的好处是,应用程序无须考虑是否要在适当的时候把控制权交给其他的线程,不必担心自己霸占处理器而导致其他线程得不到处理器时间。应用线程只要按照正常的指令流来实现自己的逻辑即可,内核会妥善地处理好线程之间共享处理器的资源分配问题。然而,这种对应用程序的便利也是有代价的,即,所有的线程切换都是在内核模式下完成的,因此,对于在用户模式下运行的线程来说,一个线程被切换出去,以及下次轮到它的时候再被切换进来,要涉及两次模式切换:从用户模式切换到内核模式,再从内核模式切换回用户模式。在Intel 的处理器上,这种模式切换大致需要几百个甚至上千个处理器指令周期。但是,随着处理器的硬件速度不断加快,模式切换的开销相对于现代操作系统的线程调度周期(通常几十毫秒)的比例正在减小,所以,这部分开销是完全可以接受的。
除了线程切换的开销是一个考虑因素以外,线程的创建和删除也是一个重要的考虑指标。当线程的数量较多时,这部分开销是相当可观的。虽然线程的创建和删除比起进程要轻量得多,但是,在一个进程内建立起一个线程的执行环境,例如,分配线程本身的数据结构和它的调用栈,完成这些数据结构的初始化工作,以及完成与系统环境相关的一些初始化工作,这些负担是不可避免的。另外,当线程数量较多时,伴随而来的线程切换开销也必然随之增加。所以,当应用程序或系统进程需要的线程数量可能比较多时,通常可采用线程池技术作为一种优化措施,以降低创建和删除线程以及线程频繁切换而带来的开销。
在支持内核级线程的系统环境中,进程可以容纳多个线程,这导致了多线程程序设计(multithreaded programming)模型。由于多个线程在同一个进程环境中,它们共享了几乎所有的资源,所以,线程之间的通信要方便和高效得多,这往往是进程间通信(IPC,Inter-Process Communication)所无法比拟的,但是,这种便利性也很容易使线程之间因同步不正确而导致数据被破坏,而且,这种错误存在不确定性,因而相对来说难以发现和调试。本书第5 章将会介绍有关线程间同步的话题。
3.2.2 线程调度算法在图3.1 中,我们看到了多个进程共享一个处理器时实际上是按照某种规则轮换执行的。当一个进程执行了一段时间以后,下一个进程被选出来占有处理器,继续它的指令流序列。这便是进程调度算法。如果一个操作系统支持内核级线程,那么,处理器资源的调度通常在线程而非进程粒度上进行。本节后面我们将介绍一些典型的线程调度算法(不完全等同于有些书籍中的进程调度或作业调度算法,原因是一些经典的进程或作业调度算法要求预先知道进程或作业的执行时间长度)。
我们首先看一下如何评价一个调度算法。衡量调度算法的准则包括多个方面,第一是公平性,调度算法在选择下一个运行的线程时,要考虑到同等地位的线程必须有相同的机会获得处理器执行权;第二是CPU 的有效利用,只要有进程或线程还在等待执行,就不能让CPU 空闲着。另外,不同类型的操作系统对于调度算法会有不同的需求,例如,实时操作系统对于响应时间有最低要求。有时候这些调度需求可能存在矛盾,比如,最大化吞吐量和最小化响应时间可能无法兼顾。
从大的分类来讲,调度算法可以分为非抢占式算法和抢占式算法。在非抢占式系统中,一个线程一旦被选择在处理器上运行,就将一直运行下去,直到阻塞(比如等待I/O 或等待一个信号量)或者自愿放弃或退出。在这类算法中,如果一个线程陷入一个长时间处理甚至无限循环的过程之中,则系统就将无法再运行其他的线程,从而整个系统可能会挂起,因此系统必须有相应的机制来打破这种可能的停滞。在抢占式系统中,一个线程被选中在处理器上运行以后,允许运行的时间长度有最大限制,一旦达到了这么长时间,就将被迫放弃执行权,交由系统挑选其他的线程来运行,或者若找不到其他的线程,则再把执行权交还给它,让它继续运行。抢占式调度算法需要一个时钟中断来获得对处理器的控制权,而非抢占式算法并不需要时钟中断。
下面介绍三种典型的线程调度算法:
(1) 先到先服务算法。在非抢占式系统中,这一算法比较自然,简单来讲,用一个FIFO(先进先出)队列就可以满足要求。所有的线程构成一个队列,最先进入队列的线程获得处理器执行权,等到放弃处理器执行权时,又回到队列尾部,下一个线程继续执行。若有新的线程进来,则添加到队列尾部。此算法简单,易于实现,但是,如果每个线程执行的任务单元所需要的时间长短不一的话,则算法的实际效果可能非常不公平。
(2) 时间片轮转调度算法。顾名思义,处理器的时间被分成了最大长度不超过某个值的时间片段,称为时间片,然后,用轮转方法分配给每一个线程。当一个线程获得了处理器执行权以后,按照自身的逻辑执行下去,直到时间片用完,或者自己主动放弃执行权(比如要等待一个信号量)。系统在获得了处理器控制权以后,用轮转方法找到下一个正在等待运行的线程,让它继续执行。这种线程调度方法实现简单,所有满足运行条件的线程排成一个队列,然后按照时间片的间隔,轮流让每一个线程获得处理器执行权。由于时钟中断每次都要打断一个线程的运行,所以,这种做法存在固有的线程切换开销,而时间片长短的选择会影响到线程切换开销所占的比例。在现代操作系统中,时间片通常设置为几毫秒到几十、上百毫秒。由于现代计算机的指令周期越来越短,线程切换开销(通常几百条指令或几千条指令,取决于算法实现的复杂程度)也在减小。这种算法使用很广泛,它不仅简单,也确实能公平地分配处理器资源。
(3) 优先级调度算法。在时间片轮转算法中,一个基本的假设是所有的线程都同等重要。这一假设在专用计算机上可能是非常合理的,但是,在现代多用途的计算机上,可能难以胜任多种不同类型的应用程序并发执行的实际情形。优先级调度算法是这种算法的一个改进,其基本思路是,每个线程都有一个优先级值,高优先级的线程总是优先被考虑在处理器上执行。操作系统在管理线程时,可以使用一个优先级队列,或者每一个优先级用一个队列来存放所有满足执行条件的线程,这样,当一个线程用完了它的时间片或者自动放弃处理器执行权时,系统选择优先级最高的线程作为下一个要运行的线程。每一个线程在队列中的位置是由它的优先级来决定的。同等优先级的线程使用轮转或先到先执行的策略。
简单优先级算法的潜在问题是,高优先级的线程可能会霸占处理器资源不放,从而导致低优先级的线程一点执行机会都没有。所以,一些变种的优先级算法考虑引入动态优先级,即每个线程有静态的优先级和动态的优先级。所谓动态的优先级是在静态优先级的基础上根据某些特定的条件提升或降低线程的优先级,系统调度器根据线程的动态优先级来安排它们的执行顺序。例如,连续执行了多个时间片的线程可能要降低优先级,而长时间没有得到时间片的低优先级线程可能会得到优先级提升。
Windows 的调度算法是一个抢占式的、支持多处理器的优先级调度算法,它为每个处理器定义了一个链表数组,相同优先级的线程挂在同一个链表中,不同优先级的线程分别属于不同的链表。当一个线程满足了执行条件时,它首先被挂到当前处理器的一个待分配的链表(称为延迟的就绪链表)中,然后调度器会在适当的时候(当它获得了控制权时)把待分配链表上的线程分配到某个处理器的对应优先级的线程链表中。当这个处理器在选择下一个要运行的线程时,会根据优先级准则选择此线程(如果没有同等或更高优先级的线程也在等待运行的话)。Windows 中线程的优先级调整考虑到了很多因素,例如前台线程、等待I/O 完成后的线程也有轻微的优先级提升,这是一些来自实践经验的设计细节,它们使得Windows 操作系统对于交互式应用程序有更好的性能表现。本章3.5 节将会详细介绍Windows 的线程调度。
3.2.3 线程与进程的关系
现在,我们来看一下线程与进程之间的关系。在不支持线程语义的系统中,进程提供了一个完全的执行环境,同时也有一个控制流完成其预定的功能,如图3.3(a)所示。操作系统按进程来分配处理器资源。在这种系统中,对线程的支持可以在用户模式下完成,即用户级线程模型,如图3.3(b)所示。在支持内核级线程的系统中,进程仅提供一个执行环境,它包含一个或者多个线程,每个线程代表一个单独的指令流,操作系统按线程来分配处理器资源,如图3.3(c)所示。Windows 中的线程是内核级线程,所以,它与进程之间的关系属于图3.3(c)所描述的情形。
(点击查看大图)图3.3 线程与进程的关系 |