本章目标CPU利用的基本单元 是多线程计算机的基础 Pthread API、Win32 API和Java线程库
一种解决方法是让服务器作为单个进程接请求收到请求时,创另一个进程处理请求 这种进程创建方法在线程流行之前很常用创进程耗时间、资源
线程在(RPC)系统中也重要第3章,RPC通过提供 类似于普通函数或子程序调用 的通信机制允许进程通信 RPC服务器是多线程。 当一个服务器接收到消息它用独立线程处理消息 这允许服务器能处理多个并发请求Java的RMI系统 也这样
现代OS都是多线程的,少数在内核中运行, 每个线程完成指定任务如 管理设备或中断处理 Solaris在内核中为 中断处理创线程, Linux用内核线程来管理 系统中的空闲内存数量
响应度高若对一个交互程序采用多线程 即使其部分阻塞或执行较冗长的操作该程序仍能继续执行增加对用户的响应程度 多线程Web浏览器 用一个线程装入图像时能通过另一线程与用户交互。
资源共享线程默认 共享它们所属进程的内存和资源。 代码和数据共享的优点: 它能允许一个应用程序在同一地址空间有多个活动线程
经济进程创建所需的 内存和资源的分配昂贵 线程能共享它所属进程的资源 so创和切线程更经济 测量进程创建和管理 与线程创建和管理的差别较困难但前常比后花更多时 Solaris 进创比线创慢30进切比线切慢5
多处理器体系结构的利用能利用 多处理器体系结构 使 每个进程能 并行 运行于不同的处理器 不管有多少CPU 单线程进程只能运行在一CPU上 在多CPU上使用多线程加强了并发功能。
多对多
多路复用了许多用户线程到同样数量或更小数量的内核线程上内核线程的数量可能与特定应用程序或特定机器有关
(多处理器上的应用程序比单处理器上分配更多数量的内核线程)多对一允许 创任意多用户线程
但是内核只能一次调度一个线程所以没增加并发性一对一提供更大的并发性
但别在应用程序内创太多(有时限制创建线程的数量)多对多模型没有这两者的缺点
开发人员可创建任意多的 用户线程且相应内核线程能在多处理器系统上并发执行且当一个线程执行阻塞系统调用时内核能调度另一个线程执行一个流行的多对多模型的变种仍然多路复用了许多用户线程到同样数量或更小数量的内核线程上,但也允许将一个用户线程绑定到某个内核线程上这个变种有时被称为二级模型(见图4.5),被IRIX、HP-UX、Tru64 UNIX等 OS支持。Solaris 9之前支持二级 但从Solaris 9用一对一
三种线程库POSIX PthreadWin32Java。PThread是POSIX标准的扩展,可提供用户级或内核级的库Win32适用于Windows OS的内核级线程库Java线程API允许 线程在Java程序中直接创建和管理由于大多数JNM实例运行在OS之上, Java线程API常用宿主系统上的线程库实现。Windows上Java线程常用Win32API,UNIX和Linux中用Pthread
设计多线程程序,在独立的线程中完成加法 s u m = ∑ i = 0 N i sum=\sum_{i=0}^{N}i sum=i=0∑Ni命令行输入加法的上限
图4.6显示部分构造一个多线程程序的基本 Pthread API,
它用独立线程计算累加和
独立线程是通过特定函数执行的
图4.6中,此函数是runner程序开始时,单个控制线程在main中开始
main创建了第二个线程并在runner中开始控制两线程共享sum该程序的详细描述pthread.h要加上pthread_t tid:线程的标识符线程都有一组属性 栈大小和调度信息 pthread_attr_t attr:线程属性 pthread_attr_init(&attr)设置这些属性 没有明确设置任何属性,故使用提供的默认属性 (5章讨论由 Pthread API提供的一些调度属性) pthread_create创线程 线程标识符线程属性函数名命令行参数argv[1]
程序有两线:main初始线程和runner执行累加的子创累加线程后,父通pthread_join,等runner线程完成累加和线程调用pthread_exit后就完成 一旦累加和线程返回父将输出累加和的值
如4.6所示的Pthread例子,独立线程共享的数据(此为sum)被声明为全局变量(DWORD数据类型是一个无符号的32位整数),还定义了一个在独立的线程中完成的Summation0函数,向该函数传递一个void指针,Win32将其定义为 LPVOID。完成此函数的线程将全局数据sum赋值为从0到传递给 Summation的参数的和。
Win32API中,线程的创建用CreateThread(如在 Pthread中那样)将一组线程的属性传递给此函数。 包括安全信息、栈大小、一个用以表明挂起状态的线程是否开始的标志。 这个程序中采用了这些属性的默认值(没有将线程初始化为挂起状态,而是使其具有被CPU调度的资格)。一旦创建了累加和线程,在输出累加值之前,父必须等待累加和线程完成,因为该值是累加和赋予的。图4.6用pthread join实现父等待累加和线程。 Win32中采用同等功能的WaitForSingleObject,使创建者线程阻塞,直至累加和线程退出(6章介绍同步对象)。
Java中两种创建线程创一个新类,从Thread类派生,并重载它的run定义一个实现Runnable接口的类。 当一类执行Runnable时,它必须定义run函数。实现run函数的代码被作为一个独立的线程执行。
Summation类实现了 Runnable接口。通过创建一个 Thread类的对象实例和传递 Runnable对象的结构来创建线程。
创建Thread对象并不会创建一个新的线程,实际上是用start函数来创建新线程。为新的对象调用 start函数需要做两件事在JM中分配内存并初始化新的线程。用run函数,使线程适合在JVM中运行(注意,从不直接调用run函数,而是调用start函数,然后它再调run)。
程序运行时,通过JVM创建两个线程。第一个是父,它在main中开始执行。第二个线程在调用 Thread对象的 start函数时创建,这个子线程在 Summation类的nun函数中开始执行。在输出累加和的值后,当此线程从mun函数中退出后线程终止。
在Win32和 Pthread中线程间共享数据很方便,因为共享数据被简单地声明为全局数据。作为一个纯面向对象语言,Java没有这样的全局数据的概念。在Java程序中如果两个或更多的线程需要共享数据,通过向相应的线程传递对共享对象的引用来实现。图4.8所示的Java程序中,main线程和累加和线程共享Sum类的对象实例,通过 getsum0和setsu)函数引用共享对象(为什么不用java.lang.Integer对象,而是设计 个新的Sum类。因为java.lang.Integer类是不可变的即一旦被赋予值,就不可改变)。
Java中的 joint函数提供了类似的功能。joint可能扔掉中断异常,这里选择忽略
JVM一般在OS之上实现(见图2.17)。 这种方式允许JVM隐藏基本的OS实现细节提供一种一致的、抽象的环境以允许Java程序能在任何支持JVM的平台上运行 JVM的规范没指明Java线程如何被映射到底层的OS,而是让特定的JVM实现来决定Windows XP采用一对一,每一个运行在这样系统上的JVM的Java线程映射到内核线程。在使用多对多模式的OS上『(Tnu64UNX),根据多对多模型来映射Java线程。Solaris刚开始多对一(如前所述的绿色线程库)来实现JVM,后来用多对多 Solaris9开始用多对多 在Java线程库和宿主操作系统线程库之间存在联系。Windows系列OS的JVM实现可以在创建Java线程时用Win32APl, Linux和 Solaris则可用 Pthread Apl
如果程序中的一个线程调用fork,那么新进程会复制所有线程,还是新进程只有单个线程?有的UNIX有两种形式的fork, 复制所有线程,只复制调用了系统调用fork的线程
exec与3章相同, 如果一个线程调用了exec,exec参数所指定的程序会替换整个进程,包括所有线程
fork的两种形式的使用与应用程序有关。如果调fork之后立即调exec,那么没有必要复制所有线程, 因为 exec参数所指定的程序会替换整个进程。这种情况下,只复制调用线程较适当。 如果fork之后另一进程并不调用exec, 那另一进程就应复制所有线程。
要取消的线程称目标线程。目标线程的取消可在如下两种情况下发生asynchronous cancellation:一个线程立即终止目标线程。deferred cancellation:目标线程不断地检査它是否应终止,这允许目标线程有机会以有序方式来终止自己
如果资源已分配给要取消的线程或要取消的线程正在更新与其他线所共享的数据,那么取消就有困难。对于异步取消尤其麻烦。操作系统回收取消线程的系统资源,但通常并不回收所有资源。因此,异步取消线程并不会使所需的系统资源空闲。相反采用延退取消时,一个线程指示目标线程要被取消,不过,只有当目标线程检査一个标志以确定它是否应该取消时才会发生取消。这允许一个线程检査它是否是在安全的点被取消, Pthread称这些点为取消点( cancellation point)。
许多实现多对多模型或二级模型的系统在用户和内核线程之间设置一种中间数据结构。 对用户线程库,LWP表现为一种应用程序可以调度用户线程来运行的虚拟处理器。每个LWP与内核线程相连该内核线程被OS调度到物理处理器上运行。如果内核线程阻塞(如在等一个IO操作结束),LWP也阻塞。在这个关系链的顶端,与LWP相连的用户线程也阻塞
为高效运行,应用程序可能要一定数量的LWP。考虑一个CPU约束的运行在单处理器上的应用程序。此时,一次只能运行一个线程,所以只要一个LWP就够了,但一个I/O请求密集的应用程序可能需要多个LWP来执行。通常,每个并发阻塞系统调用需一个LWP。5个不同文件读请求可能同时发生,此时就需5个LWP,因为每个都需要等待内核I/O的完成。如果进程只有4个LWP,那么第5个请求必须等待其中一个LWP从内核返回。
解决用户线程库与内核间通信的方法称scheduler activation它如下工作: 内核提供一组虚拟处理器(LWP)给应用程序,应用程序可调度用户线程到一个可用的虚拟处理器上。进一步说,内核必须告知与应用程序有关的特定事件。这个过程被称 upcallupcall 由具有 upcall 处理句柄的线程库处理, upcall 处理句柄必须在虚拟处理器上运行。当一个应用线程将要阻塞时,事件引发一个 upcall. 这例子中,内核向应用程序发出一个upcall通知它线程阻塞并标识特殊的线程。 然后内核分配一个新的虚拟处理器给应用程序,应用程序在这个新的虚拟处理器上运行upcl处理程序,它保存阻塞线程状态和放弃阻塞线程运行的虚拟处理器。然后 upcall 调度另一个适合在新的虚拟处理器上运行的线程,当阻塞线程事件等待发生时,内核向线程库发出另一个 upcall来通知它先前阻塞的线程现在可以运行了。此事件的 upcall处理程序也需要一个虚拟处理器, 内核可能分配一个新的虚拟处理器或先占一个用户线程并在其虚拟处理器上运行upcall 处理程序。在使非阻塞线程可以运行后,应用程序调度符合条件的线程来在一个适当的虚拟处理器上运行。
1个 Windows XP应用以独立进程运行,每个进程可包括一或多线。4.3.2讨论创建线程的Win32API。Windows XP用一对一映射,每个用户线程映射到相关的内核线程。Windows XP也提供了对fiber库的支持,该库提供多对多(见4.2.3小节)的功能。通过使用线程库,同属一个进程的每个线程都能访问进程的地址空间。一个线程包括 线程ID寄存器集合,以表示处理器状态一个用户栈,供线程在用户模式下运行:一个内核堆機,以供线程在内核模式下运行一个私有存储区域,为各种运行时库和动态链接库(DLL)所用
寄存器集合、栈和私有存储区域称为线程的上下文。线程的主要数据结枃包括:ETHREAD:执行线程块KTHREAD:内核线程块TEB:线程执行环境块。
线程所属进程的指针 线程开始控制的子程序的地址。 相应的KTHREAD指针。
线程的调度和同步信息。内核栈(当线程在内核模式下运行时使用)和TEB的指针。
E和 K完全处内核空间,只有内核可访问它们TEB是用户空间的数据结构,供线程在用户模式下运行时访问 还包括用户模式栈和用于线程特定数据的数组(Windows称为线程本地存储)。
若将FS、 VM、 SIGHAND和 FILES传给clone,父和子将共享 相同文件系统信息(如当前工作目录)、相同的内存空间、相同的信号处理程序和相同的打开文件集这种clone相当于本章介绍的创建线程,因为父和其子共享大多资源 如果调clone时没设置一个标志,则不共享, 类似fork
共享级别的变化是可能的,这源于Linux内核中任务表达的方式。系统中每个任务都有唯一的内核数据结构(task_struct), 它不保存任务本身数据,而指向其他存储这些数据的数据结构的指针如表示打开文件列表、信号处理信息和虚拟内存等的数据结构。 fork创新任务时,它具有父进的所有数据的副本当调用clone时,也创了新的任务。 不过,并非复制所有的数据结构,根据传递给fork的标志集,新的任务指向父的数据结构。
共享级别的变化是可能的,意思就是共享多少东西是可能不同的