目录
4.1 概述
线程是CPU使用的基本单元,由线程ID,程序计数器,寄存器组和堆栈组成。它与属于同一进程的其他线程共享代码段,数据段和其他操作系统资源。
4.1.1 单线程进程和多线程进程
4.1.2 多线程编程的优点
- 响应度高:一个多线程的程序即使部分阻塞,其他部分仍能运行,从而增加了对用户的响应程度。
- 资源共享:线程默认共享他们的所属进程的内存和资源。
- 经济:创建和切换线程比创建进程更节省资源和时间。
- 多处理器体系结构的利用:多线程能充分利用多处理器体系。
4.2 多线程模型
有两种方法提供线程支持:用户线程和内核线程
- 用户线程受内核支持,无须内核管理
- 内核线程由操作系统支持和管理
在用户线程和内核线程之间存在一定的关系,即多线程模型,以下讨论三种常用的关系:多对一,一对一,多对多。
4.2.1 多对一模型
多个用户线程映射到一个内核线程
- 优点:线程管理由线程库在用户空间完成,效率比较高
- 缺点:如果一个线程阻塞,整个进程就会阻塞;且多个线程无法并行运行在多处理器上
4.2.2 一对一模型
每个用户线程映射到一个内核线程上
- 优点:比多对一模型更好的并发功能;一个线程阻塞时,其他线程能够继续调用;多个线程能够并发运行在多处理器
- 缺点:创建内核线程的开销会影响应用程序的功能,系统限制了支持的线程数量。
4.2.3 多对多模型
- 多对一模型可以创建任意多的用户线程,但是没有增加并发性
- 一对一模型增强了并发性,但开发者要小心不能在应用程序中创建太多的进程
多对多模型没有上述的所有缺点,它多路复用了许多用户线程到同样数量或更小数量的内核线程上
4.2.4 双层模型(二级模型)
同时支持多对多和一对一。
4.3 线程库
线程库为程序员提供创建和管理线程的API。
实现线程库的方法有两种:
- 在用户空间中提供一个没有内核支持的库。库内的所有代码和数据结构都位于用户空间,调用库内的函数只会导致用户空间的本地函数调用,而不是系统调用。
- 实现由操作系统直接支持的内核级的一个库。库内的所有代码和数据结构都位于内核空间,调用库内的函数会导致对内核的系统调用。
目前三种主要的线程库:POSIX Pthreads、Windows、Java。
4.4 隐式多线程
将多线程的创建和管理交给编译器和运行时库来完成,这种策略称为隐式多线程。
探讨如下三种设计方法。
4.4.1 线程池
多线程服务器有一些潜在问题:第一个是关于处理请求之前用以创建线程的时间,以及线程在完成工作之后就要被丢弃这一事实。第二个,如果允许所有并发请求都通过新线程来处理,那么将没法限制在系统中并发执行的线程的数量。无限制的线程会耗尽系统资源。解决这一问题是使用线程池。
线程池的思想:在进程开始时创建一定数量的线程,并放入到池中以等待工作。当服务器收到请求时,他会唤醒池中的一个线程,并将需要服务的请求传递给他,一旦线程完成了服务,它会返回到池中再等待工作。如果池中没有可用的线程,那么服务器会一直等待,直到有空线程为止。
线程池的优点:
- 用现有线程处理请求要比等待创建新的线程要快
- 线程池限制了在任何时候可用线程的数量。
- 将执行任务和创建任务分离,允许我们采用不同的运行策略。例如延时执行、定期执行。
线程池中的线程数量由系统CPU的数量、物理内存的大小和并发客户请求的期望值等因素决定。比较高级的线程池能动态的调整线程的数量,以适应具体情况。
4.4.2 OpenMP
OpenMp为一组编译指令和API,用于编写C,C++,FORTRAN等语言的程序,支持共享内存环境下的并行编程。
OpenMP识别并行区间,即可并行运行的代码块。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。
4.4.3 大中央调度 GCD
大中央调度是Apple Mac OS X和 iOS 操作系统的一种技术,为C,C++,Swift,API,运行库的扩展,它允许应用开发人员将某些代码区段并行运行。
GCD提供调度队列来处理提交的任务:管理向GCD提交的任务并且以先进先出的顺序来执行任务
- 顺序调度队列:顺序队列中的任务同一时间只执行一件
- 并发调度队列:并发队伍中的多个任务可以并行执行
4.5 多线程问题
4.5.1 系统调用fork()和exec()
在多线程程序中,系统调用fork()和exec()的语义有所改变。
如果程序中一个进程调用fork(),那么新进程会复制所有线程,还是新进程只有单个线程?
有的UNIX系统有两种形式的fork(),一种复制所有线程,另一种只复制调用了系统调用fork()的线程。
exec()工作方式基本不变,如果一个线程调用系统调用exec(),那么exec()参数所指定的程序会替换整个进程,包括所有线程。
如果调用fork()之后立即调用exec(),那么没有必要复制所有线程,因为exec()参数所指定的程序会替换整个进程。在这种情况下,只复制调用线程比较适当。
不过,如果在fork()之后另一进程并不调用exec(),那么另一进程就应复制所有进程。
4.5.2 信号处理
信号在Unix中用来通知进程某个特定事件已经发生,信号可以同步或异步接收。
所有有信号具有同样的模式:
- 信号由特定事件的发生所产生
- 产生的信号要发送到某个进程
- 信号一旦受到就应处理。
同步信号:例如访问非法内存或被0除。在这种情况下,如果运行程序执行这些动作,那么就产生信号,同步信号发送到执行操作而产生信号的同一进程(同步的原因)。
异步信号:当一个信号由运行进程之外的事件产生,那么进程就异步接收这一信号。如使用特殊键(Ctrl + C)或者定时器到期。通常,异步信号被发送到另一个进程。
信号的处理程序有两种:
- 缺省的信号处理程序。
- 用户定义的信号处理程序。
每个信号都有一个缺省信号处理程序(默认信号处理程序),当处理信号时由内核来运行,这种默认动作可以通过用户定义的信号处理程序来改写。信号可以按照不同的方式处理。有的信号可以简单的忽略(如改变窗口大小),有的需要终止程序来处理(非法内存访问)。
单线程程序的信号处理比较直接,信号总是发送给进程。
当多线程时,信号传递到哪里去?
- 信号到信号所适用的线程
- 信号到进程内的每个线程
- 信号到进程内的某些线程
- 规定一个特定线程以接收进程的所有信号。
发送信号的方法依赖于信号的类型。
4.5.3 线程撤销
线程撤销是在线程完成之前来终止线程。
如:多线程并发执行以搜索数据库,如果一个线程得到了结果,那么其他线程可以撤销。
要撤销的线程通常称为目标线程。目标线程的撤销可在如下两种情况下发生:
一是异步撤销(asynchronous cancellation):一个线程立即终止目标线程。
二是延迟撤销(deferred cancellation):目标线程不断地检查它是否应终止,这允许目标线程有机会以有序方式来终止自己。
如果资源已经分配给要撤销的线程,或者要撤销的线程正在更新与其他线程所共享的数据,那么撤销会有困难,对于异步撤销尤为麻烦。操作系统回收撤销线程的系统资源,但是通常不回收所有资源。因此,异步撤销线程并不会使所需的资源空闲。相反采用延迟撤销时,允许目标线程检查是否处在安全的撤消点,然后再撤销。
4.5.4 线程本地存储
同属一个进程的线程共享进程数据。
在某些情况下每个线程可能需要他自己的某些数据,这种数据称为线程本地存储(线程特定数据)。可以让每个线程与其唯一的标识符相关联。