进程的延伸——线程(上)
目录
进程和线程的粗略比较
线程的使用
多线程的好处(使用场景)
构造服务器的三种方法(模型)
经典的线程模型
四个主要的线程的库函数
进程和线程的粗略比较
不同的进程拥有自己独有的一个地址空间,而在同一个进程里的多个线程(如果不是单个线程的话)共享同一个地址空间和所有可用数据。
线程的使用
1.为什么要使用(多)线程?
1)在许多应用中同时发生多种活动,其中某些活动随着时间推移会被阻塞,将这些应用程序分解成可以准并行运行的多个顺序线程,程序设计模型会变得更简单。
2)线程比进程更轻量级,它们比进程更容易创建、也更容易撤销。
3)在性能方面,如果存在大量的计算和大量的IO处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序运行的速度。
4)在多CPU系统中,多线程使真正的并行有了实现的可能。(如果是单CPU,多线程只是并发执行而非并行)
多线程的好处(使用场景)
1)考虑一个字处理软件,字处理软件可以被编写为两个线程的程序。一个线程负责与用户交互,一个线程在后台重新进行文档格式处理。如果再增加第三个线程,每隔若干分钟自动在磁盘上保存整个文件,而不必干扰其他两个进程。
试想如果是三个进程而非三个线程呢?
很显然,在这里用三个不同的进程是不能工作的,这是因为需要对同一个文件进行操作,多个线程可以共享公共内存,它们可以访问同一个正在编辑的文件,而三个进程是做不到的。
2)有关多线程作用的另一个例子是那些必须处理极大量数据的应用。通常的处理方式是,读进一块数据,对其处理,然后再写出数据。这里的问题是,如果只使用阻塞系统调用,那么在数据进入和数据
输出时,会阻塞进程。在有大量计算需要处理的时候,让CPU空转显然是浪费,应该尽可能避免。
多线程提供了一种解决方案,有关的进程可以用一个输入线程、一个处理线程和一个输出线程构造。
输入线程把数据读人到输入缓冲区中;处理线程从输入缓冲区中取出数据,处理数据,并把结果放到输
出缓冲区中:输出线程把这些结果写到磁盘上。按照这种工作方式,输入、处理和输出可以全部同时进行。当然,这种模型只有当系统调用只阻塞调用线程而不是是阻塞整个进程时,才能正常工作。
构造服务器的三种方法(模型)
1)多线程
一个称为分派程序的线程从网络中读入请求。检查请求之后,分派程序挑选一个空转(即被阻塞的)的工作线程,提交该请求。通常是在每个线程所配有的某个专门字中写入一个消息指针。接着分派线程唤醒睡眠的工作线程,使它从阻塞态转换为就绪态。
工作线程被唤醒后,它检测有关请求是否在Web页面高速缓存中,如果存在,就将该页面返回给客户机,接着该工作线程阻塞,等待一个新的请求。如果没有,工作线程就从磁盘调入该页面,将该页面返回给客户机,然后该工作线程阻塞,等待一个新的请求。
2)单线程进程
读者应该能想象它的工作流程,这里不做赘述。单线程会导致Web服务器的性能大大降低,这是不能接受的。
3)有限状态机
如果可以使用read系统调用(读磁盘操作)的非阻塞(即线程不会进入阻塞态在那等待)版本,那么就存在这第三种可能的设计。
在请求到来时,这个唯一的线程对请求进行考察。如果该请求能够在高速缓存中得到满足,那么一切都好,如果不能,则启动一个非阻塞的磁盘操作。
服务器在表格中记录当前请求的状态,然后去处理下一个事件。下一个事件可能是一个新工作的请
求,或是磁盘对先前操作的回答。如果是新工作的请求,就开始该工作。如果是磁盘的回答,就从表格
中取出对应的信息,并处理该回答。对于非阻塞磁盘I/O而言,这种回答多数会以信号或中断的形式出现。
事实上,我们以一种困难的方式模拟了线程及其堆栈。这里,每个计算都有一个被保存的状态,存在一个会发生且使得相关状态发生改变的事件集合,我们把这类设计称为有限状态机 (finite-statemachine)。有限状态机这一概念广泛地应用在计算机科学中。

笔者:这一篇篇幅较长,在此基础上增加了多线程使用场景和有限状态机的内容,主要是想让大家了解线程的基础知识外有所扩展。
经典的线程模型
知识点拨:进程模型基于两种独立的概念:资源分组处理和执行。有时,将这两种概念分开较好,于是引入了“线程”的概念。进程用于把资源集中到一起,线程则是在CPU上调度执行的实体。
由于线程具有进程的某些性质,所以有时被称为轻量级进程 (lightweight process)。
1.当多线程进程在单CPU系统中运行时,线程轮流运行。通过在多个进程之间来回切换,系统制造了不同的顺序进程并行运行的假象。多线程的工作方式也是类似的。CPU在线程之间的快速切换,制造了线程并行运行的假象,好似它们在一个比实际CPU慢一些的CPU上同时运行。在一个有三个计算密集型线程的进程中,线程以并行方式运行,每个线程在一个CPU上得到了真实CPU速度的三分之一
2.同一个进程的所有线程有相同的地址空间,这意味着它们共享同样的全局变量。一个线程可以读、写或甚至清除另一个线程的堆栈。线程之间不可能也没必要有保护。
3.和传统进程一样 (即只有一个线程的进程), 线程可以处于若干种状态的任何一个:运行、阻塞、
就绪(或终止)。这里不做赘述。
4.每个线程有其自己的堆栈(重点)。每个线程的堆栈有一帧,供各个被调用但是还没有从中返回的过程(执行指令)使用。在该帧中存放了相应过程的局部变量以及过程调用完成之后使
用的返回地址。例如,如果过程 X 调用过程 Y, 而 Y 又调用 Z, 那么当 Z 执行时,供 X、Y 和 Z 使用的栈帧会全部存在(执行这些过程的线程的)堆栈中。通常每个线程会调用不同的过程,从而有一个各自不同的执行历史,这就是为什么每个线程需要有自己的堆栈的原因。
四个主要的线程的库函数
1)thread_create
在多线程的情况下,进程通常会从当前的单个线程(主线程)开始。这个线程有能力通过调用一个库函数(如thread_create)创建新的线程。thread_create的参数专门指定了新线程要运行的过程名。这里,没有必要对新线程的地址空间加以规定,因为新线程会自动在创建线程的地址空间中运行。有时,线程是有层次的,它们具有一种父子关系,但是,通常不存在这样一种关系,所有的线程都是平等的。不论有无层次关系,创建线程通常都返回一个线程标识符,该标识符就是新线程的名字。
2)thread_exit
当一个线程完成工作后,可以通过调用一个库函数(如thread_exit)退出。该线程接着消失,不再
可调度。
3)thread_join
在某些线程系统中,通过调用一个过程,例如thread_join,一个线程可以等待一个(特定)线
程退出。这个过程阻塞调用(该过程的)线程直到那个(特定)线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止,并且也有着同样的选项。
4)thread_yield
另一个常见的线程调用是thread_yield,它允许线程自动放弃CPU从而让另一个线程运行。这样一个
调用是很重要的,因为不同于进程,(线程库)无法利用时钟中断强制线程让出CPU。所以设法使线程行为"高尚"起来,并且随着时间的推移自动交出CPU,以便让其他线程有机会运行,就变得非常重要。
引入线程的问题思考
笔者:线程这一章的内容比较多,我会分两章讲解。
考虑一下UNIX中的fork系统调用。如果父进程有多个线程,那么它的子进程也应该拥有这些线程吗?如果不是,则该子进程可能会工作不正常,因为在该子进程中的线程都是绝对必要的。
然而,如果子进程拥有了与父进程一样的多个线程,如果进程在read系统调用(比如键盘)上被
阻塞了会发生什么情况?是两个线程被阻塞在键盘上(一个下属于父进程,另一个属于子进程)吗?
在键入一行输入之后,这两个线程都得到该输入的副本吗?还是仅有父进程得到该输入的副本?或是仅有子进程得到?类似的问题在进行网络连接时也会出现。
另一类问题和线程共享许多数据结构的事实有关。如果一个线程关闭了某个文件,而另一个线程还
在该文件上进行读操作时会怎样?
假设有一个线程注意到几乎没有内存了,并开始分配更多的内存。在工作一半的时候,发生线程切换,新线程也注意到几乎没有内存了,并且也开始分配更多的内存。这样,内存可能会被分配两次。
不过这些问题通过努力是可以解决的。总之,要使多线程的程序正确工作,就需要仔细思考和设计。