作为一种提高计算机多任务处理能力的方法,多线程编程已经得到了广泛的应用,同时主流编程语言也已提供完备的支持。
然而由于任务类型、运行环境的变化,多线程程序的执行效率存在巨大的差别。因此有必要理解操作系统对于多线程任务的调度模型和相关算法,以此加深对底层运行机制的理解,产出更优秀的程序设计。
本文中的相关模型均以linux系统为例。
指令执行
首先,我们需要了解任何语言写出的代码如何被操作系统执行的。
任意语言编写的代码都将被转换成一系列汇编指令(instructions)执行,指令内容基本为对寄存器的操作,此时操作系统中的program counter(instruction pointer)将负责记录下一条指令的地址,参考下图:
线程调度
接下来思考这样一个问题,程序中创建线程的数目往往大于cpu core的数目,那么操作系统是如何做到多线程并发运行的?
事实上,在多线程程序的运行中,每个线程会在一段时间内获得cpu的使用权,直到因为任务自身原因或操作系统调度原因让出cpu使用权为止。
依据任务占有cpu的形式不同,我们可以将操作系统调度方式分为抢占式和非抢占式两种(preemptive vs. non-preemptive)。linux采用的是抢占式调度算法结合线程优先级,避免单个线程长时间占用cpu。
在linux中,线程实现层面对应的是lwp(轻量级进程),在内核中对应一个内核进程(1:1模型,部分编程语言如go实现了m:n模型),因此linux中的线程调度是由内核按照进程调度来进行的。
线程状态
在linux系统中,线程的状态如下:
waiting
线程已经停止执行并且正在等待所需资源以继续执行。通常,当线程等待网络响应、进行系统调用、尝试获取同步锁时将进入该状态,由此带来的延时也是系统性能劣化的重要根源之一。
runnable
线程已经获取了所需资源,等待被调度到cpu core运行。当大量线程处于该状态时,操作系统将依据线程优先级选择其中一个线程运行,因此如果过多的线程处于该状态,将缩短单个线程的运行时间,同样导致系统性能劣化。
executing
不解释,线程运行中。
linux系统中线程的状态转移图如下:
线程上下文切换
前面已经谈到,线程在操作系统中运行时可能由于网络io调用或主动出让时间片将cpu运行时间交给其它线程。这种交换cpu core上的线程的行为被称为上下文切换(context switch)。
上下文切换是一种成本高昂的行为,计算机各io操作的延时参考如下:
https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html
在上下文切换中,系统资源消耗主要来自两方面:
1.上下文切换时需要保存的栈信息、寄存器信息、pc信息
通常耗费1000-1500ns,现代cpu core每ns可执行12条指令,因此一次上下文切换相当于执行了12k-18k次指令
2.cpu cache miss导致的sram重新填充
cpu cache失效后需要进行重新填充,而内存的访问耗时相当于cpu cache的10倍以上
对于cpu bound类型的任务来说,增加系统中的线程数目很可能无法提高系统的performance,因为有大量的系统资源被消耗在上下文切换中。
对于io bound类型的任务来说,适当增加系统中的线程数目有利于提高系统的performance,然而考虑到线程数目的增加将导致sheduling overhead消耗时间在cpu运行时间中的占比上升,过多的线程仍然将导致系统性能下降。此时可考虑io多路复用等技术代替传统的bio提升系统性能。