人们需要多线程的主要原因是,在许多应用中同时发生着多种活动。其 中某些活动随着时间的推移会被阻塞。通过将这些应用程序分解成可以准并行运行的多个顺序线程,程序设 计模型会变得更简单。
我们通过简单的例子来介绍线程:
考虑一个字处 理软件。字处理软件通常按照出现在打印页上的格式在屏幕上精确显示文档。特别地,所有的行分隔符和页 分隔符都在正确的最终位置上,这样在需要时用户可以检查和修改文档(比如,消除孤行——在一页上不完 整的顶部行和底部行,因为这些行不甚美观)
现在考虑,如果有一个用户突然在一个有800页的文件的第一页上删掉了一个语句之后,会发生什么情 形。在检查了所修改的页面并确认正确后,这个用户现在打算接着在第600页上进行另一个修改,并键入一 条命令通知字处理软件转到该页面(可能要查阅只在那里出现的一个短语)。于是字处理软件被强制对整个 书的前600页重新进行格式处理,这是因为在排列该页前面的所有页面之前,字处理软件并不知道第600页的 第一行应该在哪里。而在第600页的页面可以真正在屏幕上显示出来之前,计算机可能要拖延相当一段时 间,从而令用户不甚满意
多线程在这里可以发挥作用。假设字处理软件被编写成含有两个线程的程序。一个线程与用户交互,而 另一个在后台重新进行格式处理。一旦在第1页中的语句被删除掉,交互线程就立即通知格式化线程对整本 书重新进行处理。同时,交互线程继续监控键盘和鼠标,并响应诸如滚动第1页之类的简单命令,此刻,另 一个线程正在后台疯狂地运算。如果有点运气的话,重新格式化会在用户请求查看第600页之前完成,这 样,第600页页面就立即可以在屏幕上显示出来
如果我们已经做到了这一步,那么为什么不再进一步增加一个线程呢?许多字处理软件都有每隔若干分 钟自动在磁盘上保存整个文件的特点,用于避免由于程序崩溃、系统崩溃或电源故障而造成用户一整天的工 作丢失的情况。第三个线程可以处理磁盘备份,而不必干扰其他两个线程
如图所示:
如果程序是单线程的,那么在进行磁盘备份时,来自键盘和鼠标的命令就会被忽略,直到备份工作完成 为止。用户当然会认为性能很差。另一个方法是,为了获得好的性能,可以让键盘和鼠标事件中断磁盘备 份,但这样却引入了复杂的中断驱动程序设计模型。如果使用三个线程,程序设计模型就很简单了
很显然,在这里用三个不同的进程是不能工作的,这是因为三个线程都需要在同一个文件上进行操作。 通过让三个线程代替三个进程,三个线程共享公共内存,于是它们都可以访问同一个正在编辑的文件
我们在举另一个例子:
一种组织Web服务器的方式如图所示。在这里,一个称为分派程序(dispatcher)的线程从网络中读入工作请求。在检查请求之后,分派线程挑选一个空转的(即被阻塞的)工作线程(worker thread),提交该请求,通常是在每个线程所配有的某个专门字中写入一个消息指针。接着分派线程唤醒睡眠的工作线程, 将它从阻塞状态转为就绪状态
在工作线程被唤醒之后,它检查有关的请求是否在Web页面高速缓存之中,这个高速缓存是所有线程都 可以访问的。如果没有,该线程开始一个从磁盘调入页面的read操作,并且阻塞直到该磁盘操作完成。当上 述线程阻塞在磁盘操作上时,为了完成更多的工作,分派线程可能挑选另一个线程运行,也可能把另一个当前就绪的工作线程投入运行,这种模型允许把服务器编写为顺序线程的一个集合。在分派线程的程序中包含一个无限循环,该循环用 来获得工作请求并且把工作请求派给工作线程。每个工作线程的代码包含一个从分派线程接收请求,并且检 查Web高速缓存中是否存在所需页面的无限循环。如果存在,就将该页面返回给客户机,接着该工作线程阻 塞,等待一个新的请求。如果没有,工作线程就从磁盘调入该页面,将该页面返回给客户机,然后该工作线 程阻塞,等待一个新的请求
现在考虑在没有多线程的情形下,如何编写Web服务器。一种可能的方式是,使其像一个线程一样运 行。Web服务器的主循环获得请求,检查请求,并且在取下一个请求之前完成整个工作。在等待磁盘操作 时,服务器就空转,并且不处理任何到来的其他请求。如果该Web服务器运行在惟一的机器上,通常情形都 是这样,那么在等待磁盘操作时CPU只能空转。结果导致每秒钟只有很少的请求被处理。可见线程较好地改 善了Web服务器的性能,而且每个线程是按通常方式顺序编程的
到现在为止,我们有了两个可能的设计:多线程Web服务器和单线程Web服务器。假设没有多线程可 用,而系统设计者又认为由于单线程所造成的性能降低是不能接受的,那么如果可以使用read系统调用的非 阻塞版本,还存在第三种可能的设计。在请求到来时,这个惟一的线程对请求进行考察。如果该请求能够在 高速缓存中得到满足,那么一切都好,如果不能,则启动一个非阻塞的磁盘操作
服务器在表格中记录当前请求的状态,然后去处理下一个事件。下一个事件可能是一个新工作的请求, 或是磁盘对先前操作的回答。如果是新工作的请求,就开始该工作。如果是磁盘的回答,就从表格中取出对 应的信息,并处理该回答。对于非阻塞磁盘I/O而言,这种回答多数会以信号或中断的形式出现
每次服务器从为某个请求工作的状态切换到 另一个状态时,都必须显式地保存或重新装入相应的计算状态。事实上,我们以一种困难的方式模拟了线程 及其堆栈。这里,每个计算都有一个被保存的状态,存在一个会发生且使得相关状态发生改变的事件集合, 我们把这类设计称为有限状态机(finite-state machine)
现在很清楚多线程必须提供的是什么了。多线程使得顺序进程的思想得以保留下来,这种顺序进程阻塞 了系统调用(如磁盘I/O),但是仍旧实现了并行性。对系统调用进行阻塞使程序设计变的较为简单,而且 并行性改善了性能。单线程服务器虽然保留了阻塞系统调用的简易性,但是却放弃了性能。第三种处理方法 运用了非阻塞调用和中断,通过并行性实现了高性能,但是给编程增加了困难
有关多线程作用的第三个例子是那些必须处理极大量数据的应用。通常的处理方式是,读进一块数据, 对其处理,然后再写出数据。这里的问题是,如果只能使用阻塞系统调用,那么在数据进入和数据输出时, 会阻塞进程。在有大量计算需要处理的时候,让CPU空转显然是浪费,应该尽可能避免。
多线程提供了一种解决方案,有关的进程可以用一个输入线程、一个处理线程和一个输出线程构造。输 入线程把数据读入到输入缓冲区中;处理线程从输入缓冲区中取出数据,处理数据,并把结果放到输出缓冲 区中;输出线程把这些结果写到磁盘上。按照这种工作方式,输入、处理和输出可以全部同时进行。当然, 这种模型只有当系统调用只阻塞调用线程而不是阻塞整个进程时,才能正常工作