Java性能 -- 协程
线程实现模型
- 轻量级进程和内核线程一对一相互映射实现的1:1线程模型
- 用户线程和内核线程实现的N:1线程模型
- 用户线程和轻量级进程混合实现的N:M线程模型
1:1线程模型
- 内核线程(Kernel-Level Thread)是由操作系统内核支持的线程,内核通过调度器对线程进行调度,负责完成线程的切换
- 在Linux中,往往通过fork函数创建一个子进程来代表一个内核中的线程
- 一个进程调用fork函数后,系统会先给新的子进程分配资源,然后复制主进程,只有少数值与主进程不一样
- 采用fork的方式,会产生大量的冗余数据,占用大量内存空间,也会消耗大量CPU时间来初始化内存空间和复制数据
- 如果是一模一样的数据,可以共享主进程的数据,于是轻量级进程(Light Weight Process,LWP)出现了
- LWP使用clone系统调用创建线程
- clone函数将部分父进程的资源的数据结构进行复制,复制内容可选,且没有被复制的资源可以通过指针共享给子进程
- LWP运行单元更小,运行速度更快,LWP和内核线程一一映射,每个LWP都是由一个内核线程支持
N:1线程模型
- 1:1线程模型的缺陷
- 在线程创建、切换上都存在用户态和内核态的切换
- 系统资源有限,无法支持创建大量LWP
- 该线程模型在用户空间完成了线程的创建、同步、销毁和调度,并不需要内核的帮助,不会产生用户态和内核态的空间切换
N:M线程模型
- N:1线程模型的缺陷
- 操作系统无法感知用户态的线程,容易造成某个线程进行系统调用内核线程时被阻塞,从而导致整个进程被阻塞
- N:M线程模型是一种混合线程管理模型
- 支持用户态线程通过LWP与内核线程连接,用户态的线程数量和内核态的LWP数量是N:M的映射关系
Java线程 / Go协程
- Java线程
- Thead#start通过调用native方法start0实现
- 在Linux下,JVM Thread是基于pthread_create实现的,而pthread_create实际上调用了clone系统调用来创建线程
- 所以,Java在Linux下采用的是1:1线程模型(用户线程与轻量级线程一一映射),线程通过内核调度,涉及上下文切换
- Go协程
- Go语言使用了N:M线程模型实现了自己的调度器,在N个内核线程上多路复用M个协程
- 协程的上下文切换在用户态由协程调度器完成,不需要陷入到内核,相比Java线程,代价很小
协程的实现原理
- 协程可以看作一个类函数或者一块函数中的代码,可以在主线程里面轻松创建多个协程
- 程序调用协程和调用函数是不一样的,协程可以通过暂停或者阻塞的方式将协程的执行挂起,而其他协程可以继续执行
- 协程的挂起只是在程序中(用户态)的挂起,同时将代码执行权转让给其他协程使用
- 待获取执行权的协程执行完之后,将从挂起点唤醒挂起的协程
- 协程的挂起和唤醒是通过一个调度器完成的
图例解释
- 假设程序中默认创建两个线程为协程使用,在主线程中创建协程ABCD…,分别存储在就绪队列中
- 调度器首先会分配工作线程A执行协程A,工作线程B执行协程B,其他创建的协程将会在等待队列中进行排队等待
- 当协程A调用暂停方法或被阻塞时,协程A会进入到挂起队列,调度器会调用等待队列中的其他协程抢占线程A执行
- 当协程A被唤醒时,它需要重新进入到就绪队列中,通过调度器抢占线程
- 如果抢占成功,就继续执行协程A;如果抢占失败,就继续等待抢占线程
线程 / 协程
- 相比于线程,协程少了由于同步资源竞争带来的_CPU上下文切换_
- 应用场景:_IO阻塞型场景_
- 比较适合IO密集型的应用,特别在网络请求中,有较多的时间在等待服务端响应
- 协程可以保证线程不会阻塞在等待网络响应(可以在协程层面阻塞)中,充分利用了多核多线程的能力
- 对于CPU密集型的应用,由于多数情况下CPU都比较繁忙,协程的优势就不会特别明显
- 比较适合IO密集型的应用,特别在网络请求中,有较多的时间在等待服务端响应
- 线程是通过共享内存的方式来实现数据共享,而协程是使用了通信(MailBox)的方式来实现数据共享
- 这主要为了避免内存共享数据而带来的线程安全问题
小结
- 协程可以认为是运行在线程上的代码块,协程提供的挂起操作会使协程暂停执行,而不会导致线程阻塞
- 协程是一种轻量级资源,即使创建上千个协程,对系统来说也不会是很大的负担,而线程则不然
- 协程的设计方式极大地提高了线程的使用率
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.