Java协程框架--Kilim工作原理 - Go语言中文社区

Java协程框架--Kilim工作原理


Kilim要解决的问题

Kilim协程框架中最核心需要解决的问题:

  1. 如何暂停处理当前任务,转而处理其他任务?
  2. 如何恢复任务继续执行?

也即如何实现协程本身的 yield / resume的语义特性。

Kilim的解决方案

概括的讲,​Kilim框架在实现这个语义特性时,干了以下几个事情:

  • 利用字节码技术(基于ASM字节码框架),将普通代码转化为支持协程的代码;
  • 调用Pauseable方法的时候,如果暂停了就保存当前方法栈的State,暂停执行当前Task,将控制权交给Scheduler调度器;
  • Scheduler调度器负责协调其他就绪的Task;
  • 之前暂停的Task恢复的时候,自动恢复State,恢复到上次执行的位置继续执行;

其中,第一点是在编译期实现,后面三点是在运行期执行。

再稍微详细一点就是:Kilim通过编译期字节码编织,对每一个可暂停(Pauseable)的方法进行字节码处理,在方法执行前和执行后加上相关的执行上下文的处理,暂停时会保存整个线程堆栈,然后通过特定的字节码跳转指令goto跳转到另外一个Task的执行方法中,恢复时将复原整个线程堆栈,回到上次暂停时的位置继续往下执行。

Kilim的工作原理

第一个Kilim最神奇的地方在于字节码增加,那么它是怎样将普通的Java代码改写层支持协程的代码呢?首先上Kilim官方文档中的一张图:


这张图也即Kilim实现协程语义的精髓所在,我们来一一分析。

左边是普通的Java函数代码,与我们常见的函数唯一有所不同的是函数a和b均显示声明抛出Pausable异常,而实际上这个异常在运行期间不会抛出,它的实际作用类似于注解,使得Kilim能够识别哪些代码需要Weaver工具进行代码增强。函数抛出Pausable异常即表明该函数是可暂停的,

右边的代码即通过字节码增强后的代码,与左边原始的代码相比,首先函数声明中额外增加了一个Fiber参数,Fiber可以理解为当前纤程、协程的上下文。Fiber中存储着协程暂停和恢复时需要用到的函数堆栈、程序计数器以及当前函数的执行状态。字节码增强后的代码以调用Pausable方法a为分界,将整个函数分成几个代码块,也即官网文档中提及的prelude、pre_call、post_call三部分。

1.在prelude块中,也即刚进入函数a时将会执行的代码块,将根据Fiber中的pc程序计数器跳转到对应的代码块处开始执行。

2.在pre_call块中,也即在调用函数b之前,将调用Fiber的down方法记录当前执行状态和pc程序计数器,标识着函数将进入下一个Pausable方法。

3.在post_call块中,也即在调用函数b之后,将调用Fiber的up方法计算函数b调用完成后返回的状态,标识着从一个被调的Pausable方法返回,它既可能是正常的函数b执行完成返回,也可能是函数b执行暂停返回,接着通过这个状态控制后续的执行流程。

这四种状态分别为:

  • NOT_PAUSING__NO_STATE,即被调函数执行完成正常返回,这种情况与即普通的函数执行类似。
  • NOT_PAUSING__HAS_STATE,即被调函数执行完成,但还存在上次暂存的栈帧,这种情况一般是函数从上次暂停处恢复执行,且顺利执行完成返回,此时需要恢复函数的栈帧,然后goto到RESUME代码块继续执行。
  • PAUSING__NO_STATE,即被调函数执行过程中暂停,且还未保存函数栈帧,需要主调函数执行暂存操作,这种情况一般即第一次协程执行到需要暂停处,此时需要采用字节码暂存函数的栈帧和状态,然后直接return。
  • PAUSING__HAS_STATE,即被调函数执行过程中暂停,且已经保存函数栈帧,这种情况是该Pausable从上次暂停处恢复执行,但是依然没有预期的结果,需要再次暂停,此时因为之前暂停时函数栈帧和状态都已经保存过,不需要再做什么,因此直接return即可。


ok,到目前为止是不是谜底已经大概清晰,不过要把协程与线程的执行过程整个串联起来,形成一个整体还稍显迷惑,接下来详细说明。

上面已经提到协程执行过程中核心的两个点:一点是调用Pauseable方法的时候,如果暂停了就保存当前方法栈的State,暂停执行当前Task,将控制权交给Scheduler调度器,另外一点是暂停的Task恢复的时候,自动恢复State,恢复到上次执行的位置继续执行。这两点的具体过程如下:

上篇博文Java协程框架--Kilim源码分析中已经讲到在Kilim中有几个核心元素,包括Task、Scheduler、WorkerThread以及Mailbox。其中WorkerThread即实际执行Task任务的工作者线程,Task即具体的可暂停的业务,Task与Task之间通过定制的Mailbox来通信。Kilim将线程run方法体中所有嵌套层级调用的所有Pausable方法组织成一个具有父子关系的调用链,形如run->A->B->C,通过Task私有的Fiber来记录执行到哪一个层级。

通常Kilim中大部分使用Mailbox提供的get、getb、getnb三个不同版本来接收消息,其中最常用的get会阻塞当前Task而不阻塞当前线程。

那么如何实现Task的暂停呢?

例如一个运行状态Task的调用链run->A->B->C,其中A、B、C均为Pauseable方法,在函数C中调用了Mailbox的get方法且设置了超时时长,当整个链嵌套执行到C的get方法这一行时,因为get本身也是一个Pausable方法,如果没有接收到消息,将会把Task作为该Mailbox的观察者,并调用Task.pause(this)方法暂停自身,然后该get方法即直接返回,get调用返回后,C根据Fiber的up计算发现是暂停返回,则也暂停本函数,暂存栈帧和状态,直接返回,如此逆向直到run方法,从而实现Task的暂停。

那么又如何实现Task的恢复呢?

Task暂停的过程中有一步很关键,将该Task作为该Mailbox的观察者,在当有其他线程把消息通过调用Mailbox的put方法添加到Mailbox中时,或者超时定时器触发时,将会回调该Mailbox的观察者,告诉观察者有新的消息到来。这样Task的onEvent被回调,onEvent直接调用resume方法,而resume方法实际最重要的一步即调用Scheduler的schedule方法,将该Task加入到Scheduler的可运行任务队列中,并随机选择一个等待运行的工作者线程,并notify该线程,该线程被唤醒后将执行该Task,重复之前的函数调用链run->A->B->C执行,由于A、B、C三个函数中均已经保存了之前暂停的函数栈帧和状态,因此之前已经执行过的代码块将不会重复执行,会根据Fiber中状态选择性的执行对应的代码块。因为Mailbox中已经有消息,因此再重复执行到get方法时能够直接获取到消息,正常的往下继续执行。这样相当于又走了一次调用链,但是并非重复执行已经执行过的代码,而是恢复执行之前未执行的代码,从而实现Task的恢复。

    public void onEvent(EventPublisher ep, Event e) {
        resume();
    }


版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/kobejayandy/article/details/41413095
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-06-27 23:02:57
  • 阅读 ( 1219 )
  • 分类:Go Web框架

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢