思维导图:
1.线程:
线程可以视为进程中的控制流.一个进程至少会包含一个线程.因为其中至少会有一个控制流持续运行.一个进程的第一个线程会随着这个进程的启动而创建.这个线程称为该进程的主线程.当然.一个进程也可以包含多个进程.这些线程都是由当前进程中已存在的线程创建出来的.创建的方法就是调用系统调用.更确切的说是调用pthread_create函数.拥有多个线程的进程可以并发执行多个任务.并且即使某个或某些任务被阻塞.也不会影响其他任务正常执行.这可以大大改善程序的响应时间和吞吐量.另一方面.线程不可能独立于进程存在.它的生命周期不可能逾越进程的生命周期.
一个进程中的所有线程都拥有自己的线程栈.并以此存储自己的私有数据.这些线程的线程栈都包含在其所属进程的虚拟内存地址中.要注意.一个进程中的很多资源都会被其中的所有线程共享.这些被线程共享的资源包括在当前进程的虚拟内存地址中存储的代码段 数据段 堆 信号处理函数.以及当前进程所持有的文件描述符等等.所以.同一进程中的多个线程运行的一定是同一个程序.只不过具体的控制流程和执行的函数可能会不同.在同一个进程的多个线程之间共享数据也是非常轻松和自然的事情,
2.线程标识:
和进程一样.每个线程也有属于自己的ID.这类ID也称为线程ID或者TID.但与进程不同.线程ID在系统范围内可以不唯一.而只在其所属进程范围内唯一.不过.Linux系统的线程实现则确保了每个线程ID在系统范围内的唯一性.并且当线程不复存在后.其线程ID可以被其他线程复用.
3.线程间的控制:
线程中的每个进程都有它的父进程.而由某个进程创建出来的进程称为该进程的直接子进程.与这种家族式的树状结构不同.同一个进程中的任意两个线程之间的关系都是平等的.它们之间不存在层级关系.任何线程都可以对同一进程中的其他线程进行有限的管理.这里的有限管理主要有以下四种.
创建线程:
主线程在其所属进程启动时创建.因此.它的创建并不在此论述范围内.这里仅指对其他线程的创建.任何线程都可以通过调用系统调用pthread_create来创建新的线程.为了方便理解.把调用系统调用或函数的线程称为调用线程.在创建线程时.调用线程需要给定新线程将要执行的函数以及传入该函数的参数值.由于代表函数的参数被命名为start.start函数是可以有返回值的.可以在其他线程中通过与新线程的连接得到在该新线程中执行的start函数的返回值.如果新线程创建成功.调用线程会得到新线程的ID.
终止线程:
线程可以通过多种方式终止同一进程中的其他线程.其中一种方式就是调用pthread_cancel.该函数的作用是取消掉给定线程ID代表的那个线程.明确的讲.它会向目标线程发出一个请求.要求它立即终止执行.但是.该函数只是发送请求并立即返回.而不会等待目标线程对该请求做出响应.至于目标线程什么时候做出响应,做出怎样的响应,则取决于另外的因素(比如目标线程的取消状态及类型).默认情况下.目标线程总是会接收线程取消请求.不过等时机成熟(执行到某个取消点)的时候.目标线程才会去响应线程取消请求.
连接已终止的线程:
此操作由操作系统调用pthread_join来执行.该函数会一直等待与给定的线程ID对应的那个线程终止.并把该线程执行的start函数的返回值告知调用线程.如果目标线程已经处于终止状态.那么该函数会立即返回.这就像把调用线程放置在了目标线程后面.当目标线程把流程控制权交出时.调用线程会接过流程控制权并继续执行pthread_join函数调用之后的代码.这也是把这一操作称为连接的缘由之一.如果一个线程可以连接.那么在它终止之时就必须连接.否则就会变成一个僵尸线程.僵尸线程不但会导致系统资源浪费.还会无意义的减少其所属进程的可创建线程数量.
分离线程:
将一个线程分离意味着它不在是一个可连接的线程.而在默认情况下.一个线程总可以被其它线程连接.分离操作的另一个作用是让操作系统内核在目标线程终止时自动进行清理和销毁工作.注意.分离操作是不可逆的.也就是说.无法使一个不可连接的线程变回到可连接的状态.不过.对于一个已处于分离状态的线程.执行终止操作仍会起作用.分离操作由系统调用pthread_detach来执行.它接受一个代表了线程ID的参数值.
一个线程对自身也可以进行两种控制.终止和分离.线程终止自身的方式有很多种.在线程执行的start函数中执行return语句.会使该线程随着start函数的结束而终止.需要注意的是.如果在主线程中执行了return语句.那么当前进程中的所有线程都会终止.另外.在任意线程中调用exit也会打到这种效果.还有一种终止自身的方式是.显示的调用pthread_exit函数.那么只有主线程自己会终止.其他线程仍会照常运行.这是很重要的区别.线程分离自身与分离其他线程的方式并无不同.即调用pthread_death函数.区别仅在于调用线程传递给该函数的ID是自己的ID还是其他线程ID.
4.线程的状态:
一个线程从创建到终止的完整生命周期中也经常会在多个状态之间切换.由于线程只是进程中的一个控制流.所以对进程的状态描述几乎都适用于线程.
线程创建出来之后.就会进入就绪状态.处于就绪状态的线程会等待运行时机.一旦该线程被真正运行.就会由就绪状态转换至运行状态.正在运行的线程可能会由于某些原因阻塞进而由运行状态转换至睡眠状态.这里可能的原因包括但不限于等待未完成的I/O操作.等待还未接收到的信号 等待获得互斥量.以及等待某个条件变量.后两个原因都属于因同步而产生的线程阻塞.当阻塞线程等待的那个事件发生或条件满足时.该线程就会被唤醒.这时会从睡眠状态转出.但并不会直接进入运行状态.而是先进入就绪状态再次等待运行时机.如果CPU正处于空闲状态.那么它会立即执行.处于运行状态的线程有时也会因为CPU被其他线程抢占而失去运行时机.从而转回至就绪状态并等待下一个运行时机.操作系统内核的调度器会按照一定的算法和策略使线程在这三个状态之间转换.
在当前线程自我终结或其他线程向当前线程发出取消请求且取消时机已到之后.当前线程就会试图进入终止状态.如果当前线程之前没有分离过,并且此时并没有其他线程与它连接.那么当前线程会进入僵尸状态而不是终止状态.当且仅有其他线程与之连接之后.当前线程才会从僵尸状态转换至终止状态.处于终止状态的线程才会被操作系统内核回收.有两种操作可以直接使当前线程进入终止状态.而不管它是否分离.在任意线程中调用exit函数以及在主线程中执行return语句.不但会使其所属进程中的所有线程立即终止.还会结束进程的运行.
5.线程的调度:
在线程的生命周期中.操作系统内核对线程的调用是非常核心的部分.正因为有了调度器的实时调度和切换.才给我们一种众多线程被并行运行的幻觉.调度器会把时间划分成极小的时间片并把这些时间片分给不同的线程.以使众多线程都有机会在CPU上运行.一个线程什么时候能够获得CPU时间.以及它能够在CPU上运行多久.都属于调度器的工作范畴.线程调度(也称为线程间的上下文切换)是一项非常复杂的工作.这里只对线程调度的最基本规则和策略进行阐述.
线程的执行总是趋向于CPU受限或I/O受限.一些线程需要花费一定的时间使用CPU进行计算.而另外一些线程则会花费一定的时间等待相对较慢的I/O操作的完成.一个用于计算16位整数的14次方属于前者.而一个等待人类用户通过敲击键盘提供输入数据的线程则属于后者.通常情况下.一个线程的趋向性并不那么清晰.因此.调度器往往需要猜测它们.这是非常困难的任务.调度器会依据它对线程的趋向性猜测它们的分类.并让I/O受限的线程具有更高的动态优先级以优先使用CPU.
注意:刚刚所说的线程的动态优先级是可以被调度器实时调整的.而与之相对应的线程的静态优先级则只能由应用程序指定.如果应用程序没有显示指定一个线程的静态优先级.那么它将被设为0.调度器不会改变线程的静态优先级.线程的动态优先级就是调度器在其静态优先级的基础上调整得出的.它在线程的运行顺序上起到了关键作用.而线程的静态优先级则决定了线程单次在CPU上运行的最长时间.也就是调度器分配给它的时间片大小.
所有等待使用CPU的线程都会按照动态优先级从高到低的顺序排列.并依序放到与该CPU对应的队列找那个.因此.下一个运行的线程总是动态优先级最高的那一个.实际上.每一个CPU运行队列中都包含两个优先级阵列.其中一个用于存放正在等待运行的线程.暂且称之为激活的优先级阵列.另一个则用于存放已经运行过但未完成的线程.暂且称之为过期的优先级队列.
下一个运行的线程总是会从激活的优先级队列选出.如果调度器发现某个线程已经占用了CPU很长时间(该时间只会小于或等于给予该线程的时间片).并且激活的优先级阵列中还有优先级与它相同的线程等待运行.那么调度器就会让那个等待线程在CPU上运行.而被换下的下线程会被排入过期的优先级阵列.当激活的优先级阵列中没有等待运行的线程时.调度器会把这两个优先级阵列身份互换.
线程不总是会在就绪状态和运行状态之间徘徊.它还有可能被阻塞而进入睡眠状态.处于睡眠状态的线程不能够被调用和运行.它们会从运行队列中移除.线程的睡眠状态也可以细分为可中断的睡眠状态和不可中断的睡眠状态.
线程会因为等待某个事件或条件的发生而加入到对应的等待队列中.并随即进入睡眠状态.当事件发生或条件满足时.内核会通知对应的等待队列中的所有线程.这些线程会因此而被唤醒进入等待队列转移至适当的运行队列中.调度器往往会稍稍调高被唤醒线程的动态优先级.以使这类线程能够早早运行.
如果当前计算机上有多个CPU.那么平衡它们之间的负载也将会是调度器的职责之一.调度器会尽量使一个线程在一个特定的CPU上运行.这样做很多好处.比如维持高速缓存的命中率以及高效率使用就近内存.有时候.一个CPU需要运行太多的线程以至于造成了多CPU之间的不平衡.在这种情况下.调度器会把一些原本较忙碌CPU上运行的线程迁移至其他较空闲的CPU上运行.
总体来说.操作系统内核的调度器就是使用若干策略对众多线程在CPU上的运行进行干涉.以使得操作系统中的各个任务都能够有条不紊的进行.同时.还要兼顾效率和公平性.从线程的角度看.调度器是通过协调各个线程状态来达到调度目的.
6.线程实现模型:
线程的实现模型主要有三个.分别是.用户级线程模型 内核级线程模型和两级线程模型.它们之间的差异就在于线程与内核调度实体(简称KSE)之间的对应关系上.
用户级线程模型:
此模型下的线程是用户级别的线程库全权管理.线程库并不是内核的一部分.而只是存储在进程的用户空间中.这些线程的存在对于内核来说是无法感知的.这些线程也不是内核调度器的对象.对线程的各种管理和协调完全是用户级程序的自主行为.与内核无关.应用程序在对线程进行创建 终止 切换或同步等操作的时候.并不需要让CPU从用户态切换到内核态.从这方面讲.用户级线程模型确实在线程操作的速度上存在优势.并且由于对线程的管理完全不需要内核的参与.所示使得程序可移植性更强一些.但是这一特点导致在此模型下的多线程并不能够真正并发运行.例如.如果线程在I/O操作过程中被阻塞.那么其所属进程也会被阻塞.这正是由线程无法被内核调度造成的.在调度器眼里.进程是一个无法在被分割的调度单元.无论其中存在多少个线程.另外.即使计算机上存在多个CPU.进程中的多个线程也无法分配给不同的CPU运行.对于CPU负载均衡来说.进程的粒度太粗了.因而让不同的进程在不同的CPU上运行也微乎其微.线程的优先级也会形同虚设.同一个进程中所有的线程的优先级只能由该进程优先级来体现.线程库对线程的调度完全不受内核控制.它与内核为进程设定的优先级也没关系.正因为用户级线程存在这一严重的缺陷.所以现代操作系统都不使用这种模型来实现线程.在早期.以这种模型作为线程实现方式的案例确实存在.由于包含了多个用户级线程的进程只与一个KSE相对应.因此这种线程模型又称为多对一(M:1)的线程实现.
内核级线程模型:
该模型下的线程是由内核负责管理的.它们是内核的一部分.应用程序对线程的创建 终止和同步都必须通过内核提供的系统调用来完成.进程中的每一个线程都与KSE相对应.也就是说.内核可以分别对每一个线程进行调度.由此.内核级线程模型又称为一对一(1:1)线程实现.一对一线程实现消除了多对一线程的很多弊端.可以真正的实现程序的并发运行.因为这些线程完全由内核管理调度.内核可以在不同的时间片内让CPU运行不同的线程.内核在极短的时间内快速切换和运行各个线程.使得它们看起来像在同时运行.即使进程中的一个线程由于某种原因进入到阻塞状态.其他线程也不会受到影响.这也使得内核在多个CPU上进行负载均衡变得从容有效.如果一个线程与被阻塞的线程之间存在同步关系.也可能受到牵连.但是这是一种应用级别的干预.并不属于线程本身的特质.但是内核线程明显比用户级线程管理成本要高很多.线程的创建会用到更多地内核资源.并且.像创建线程 切换线程以及同步线程这类操作会花费的时间也会更多.如果一个进程包含了大量的线程.会给内核调度器带来巨大的负担.甚至会影响操作洗的整体性能.因此.采用内核级线程模型的操作系统对一个进程可创建的线程数量都有直接或间接的限制.尽管内核级线程模型有资源消耗巨大 调度速度较慢的缺点.但与用户级线程的实现相比.还是有较大优势.很多现代操作系统都是以内核级线程模型实现的.包括linux系统.实际上.Linux系统最新线程库实现(NPTL)为最小化内核级线程模型的劣势付出了巨大努力.这也使得Linux操作系统中使用线程更加高效.
两级线程模型:
两级线程模型的目标是取前两种模型之精华.并去二者糟粕.也称为多对多(M:N)的线程实现.与其他模型相比.两级线程模型提供了更多的灵活性.在此模型下.一个进程可以与多个KSE相关联.这与内核级线程模型类似.但与内核级线程模型不同的是.进程中的线程(以下称为应用程序线程)并不与KSE关联.这些应用程序线程可以映射到同一个已关联的KSE上.首先.实现了两级线程模型的线程库会通过操作系统内核创建多个内核级线程.然后它会通过这些内核级线程对应用程序线程进行调度.大多数此类线程库都可以将这些应用程序线程动态的与内核线程进行关联.这样的设计显然使线程的管理工作更加复杂.因为这需要内核和线程库共同努力和协作才能正确 有效的进行.但是由于这样的设计.内核资源的消耗才得以大大的减少.同时也使线程管理操作的效能提高不少.因为两级线程实现的复杂性.它往往不会被操作系统的内核开发者使用.这样的模型却可以在编程语言层面充分发挥出作用.Go就是最好代表.
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
昨天到今天.仿佛跨越了世纪.
如果喜欢我的分享的话.可以关注我的微信公众号
念何架构之路