Java性能 -- 协程

线程实现模型

  1. 轻量级进程内核线程一对一相互映射实现的1:1线程模型
  2. 用户线程内核线程实现的N:1线程模型
  3. 用户线程轻量级进程混合实现的N:M线程模型

1:1线程模型

  1. 内核线程(Kernel-Level Thread)是由操作系统内核支持的线程,内核通过调度器对线程进行调度,负责完成线程的切换
  2. 在Linux中,往往通过fork函数创建一个子进程来代表一个内核中的线程
    • 一个进程调用fork函数后,系统会先给新的子进程分配资源,然后复制主进程,只有少数值与主进程不一样
  3. 采用fork的方式,会产生大量的冗余数据,占用大量内存空间,也会消耗大量CPU时间来初始化内存空间和复制数据
  4. 如果是一模一样的数据,可以共享主进程的数据,于是轻量级进程(Light Weight Process,LWP)出现了
    • LWP使用clone系统调用创建线程
    • clone函数将部分父进程的资源的数据结构进行复制,复制内容可选,且没有被复制的资源可以通过指针共享给子进程
    • LWP运行单元更小运行速度更快,LWP和内核线程一一映射,每个LWP都是由一个内核线程支持

N:1线程模型

  1. 1:1线程模型的缺陷
    • 在线程创建、切换上都存在用户态和内核态的切换
    • 系统资源有限,无法支持创建大量LWP
  2. 该线程模型在用户空间完成了线程的创建、同步、销毁和调度,并不需要内核的帮助,不会产生用户态和内核态的空间切换

N:M线程模型

  1. N:1线程模型的缺陷
    • 操作系统无法感知用户态的线程,容易造成某个线程进行系统调用内核线程时被阻塞,从而导致整个进程被阻塞
  2. N:M线程模型是一种混合线程管理模型
    • 支持用户态线程通过LWP内核线程连接,用户态的线程数量内核态的LWP数量是N:M的映射关系

Java线程 / Go协程

  1. Java线程
    • Thead#start通过调用native方法start0实现
    • 在Linux下,JVM Thread是基于pthread_create实现的,而pthread_create实际上调用了clone系统调用来创建线程
    • 所以,Java在Linux下采用的是1:1线程模型(用户线程与轻量级线程一一映射),线程通过内核调度,涉及上下文切换
  2. Go协程
    • Go语言使用了N:M线程模型实现了自己的调度器,在N个内核线程上多路复用M个协程
    • 协程的上下文切换在用户态协程调度器完成,不需要陷入到内核,相比Java线程,代价很小

协程的实现原理

  1. 协程可以看作一个类函数或者一块函数中的代码,可以在主线程里面轻松创建多个协程
  2. 程序调用协程和调用函数是不一样的,协程可以通过暂停或者阻塞的方式将协程的执行挂起,而其他协程可以继续执行
    • 协程的挂起只是在程序中(用户态)的挂起,同时将代码执行权转让给其他协程使用
    • 待获取执行权的协程执行完之后,将从挂起点唤醒挂起的协程
    • 协程的挂起唤醒是通过一个调度器完成的

图例解释

  1. 假设程序中默认创建两个线程为协程使用,在主线程中创建协程ABCD…,分别存储在就绪队列
  2. 调度器首先会分配工作线程A执行协程A,工作线程B执行协程B,其他创建的协程将会在等待队列中进行排队等待
  3. 当协程A调用暂停方法被阻塞时,协程A会进入到挂起队列,调度器会调用等待队列中的其他协程抢占线程A执行
  4. 当协程A被唤醒时,它需要重新进入到就绪队列中,通过调度器抢占线程
    • 如果抢占成功,就继续执行协程A;如果抢占失败,就继续等待抢占线程

线程 / 协程

  1. 相比于线程,协程少了由于同步资源竞争带来的CPU上下文切换
  2. 应用场景:IO阻塞型场景
    • 比较适合IO密集型的应用,特别在网络请求中,有较多的时间在等待服务端响应
      • 协程可以保证线程不会阻塞在等待网络响应(可以在协程层面阻塞)中,充分利用了多核多线程的能力
    • 对于CPU密集型的应用,由于多数情况下CPU都比较繁忙,协程的优势就不会特别明显
  3. 线程是通过共享内存的方式来实现数据共享,而协程是使用了通信(MailBox)的方式来实现数据共享
    • 这主要为了避免内存共享数据而带来的线程安全问题

小结

  1. 协程可以认为是运行在线程上的代码块,协程提供的挂起操作会使协程暂停执行,而不会导致线程阻塞
  2. 协程是一种轻量级资源,即使创建上千个协程,对系统来说也不会是很大的负担,而线程则不然
    • 协程的设计方式极大地提高了线程的使用率
0%