作者:ScratchLab
链接:https://www.zhihu.com/question/308641794/answer/2867920715
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
线程有两种实现方式:内核态线程和用户态线程。早期,内核态线程由于概念清晰,对开发者友好,在与用户态线程的竞争中胜出。但随着互联网的发展,用户态线程凭借其线程切换成本低、竞态少等特点重新回归开发者视野,并逐步发展成最新的并发模型--协程。下面我们从线程切换和竞态两个方面介绍一下内核态线程和用户态线程。从线程切换的角度来讲,进程与线程基本原理是一样的。下图展示了内核态线程切换的一个大概的过程:<img src="https://pic1.zhimg.com/50/v2-170cedfa1203204c13fafc91283bd031_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="467" data-rawheight="501" class="origin_image zh-lightbox-thumb" width="467" data-original="https://pic1.zhimg.com/v2-170cedfa1203204c13fafc91283bd031_r.jpg?source=1940ef5c"/>当前时刻,线程A正在运行。此时,来了一个时钟中断,系统由ring3的线程A跳转到ring0的时钟中断handler中。当handler认为需要切换线程时,会将线程A的上下文保存到线程A的控制块中。然后,handler根据调度算法从就绪线程中选择一个来运行,假设handler选择了线程F来运行。handler会将线程F的上下文从其控制块中载入到当前线程。完成上下文保存/载入工作后,handler退出,并跳转到ring3。此时,ring3中运行的就是线程F了。从上述流程我们可以发现,内核态线程有以下特点:线程切换的时机由操作系统决定(抢占式),线程无法对切换时机做任何假设。因此,多线程程序开发时必须考虑竞态线程切换时涉及到特权级的跳转和线程上下文的保存/载入这就造成了内核态线程切换时的成本非常高,线程数量多时,线程切换的开销甚至能超过业务代码。下面,我们看一下用户态线程的切换过程。下图展示了用户态线程切换的一个大致过程:<img src="https://picx.zhimg.com/50/v2-1fe9d8624b94078935205e93d828dca6_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="456" data-rawheight="368" class="origin_image zh-lightbox-thumb" width="456" data-original="https://picx.zhimg.com/v2-1fe9d8624b94078935205e93d828dca6_r.jpg?source=1940ef5c"/>当前时刻,线程A正在运行。线程A运行一段时间后主动退出,将其上下文保存到线程A的控制块中。然后,线程A根据用户代码从其他线程中选择一个来运行。假设用户代码要求线程A退出后线程F继续运行。线程A会将的线程F的上下文载入到当前线程中,并跳转到线程F的代码中运行。各用户态线程不断的运行、退出,形成这样一个序列:A线程运行A线程退出,选择F来运行F线程运行F线程退出,选择D来运行D线程运行D线程退出,选择E来运行...A线程运行A线程退出,选择B来运行从以上介绍中我们可以看到,没有了时钟中断,某个线程运行时无法被强制退出,只有主动退出,其他线程才有运行机会。用户态线程的调度就依靠各线程在合适的时机主动退出,让其他线程获得运行机会来进行。各用户态线程彼此协作,推动程序的运行,因此,用户态线程又称作协程。从上述流程我们可以看出,用户态线程有以下特点:各用户态线程本质上是在一个单线程进程上执行的,线程调度的时机由用户代码完全控制,因此不用考虑竞态线程切换过程不涉及特权级的跳转线程切换时也涉及到上下文的保存/载入,但是各用户态线程是在一个单线程进程上运行的,可以共享许多数据,因此用户态线程上下文的数据量远远小于内核态线程上下文从以上特点我们可以看到,用户态线程切换的开销非常低,且系统不会限制用户态线程的数量,非常适合高并发。