java里的协程

时间:2025-03-30 10:38:13
这东西其实有很多名词,比如有的人喜欢称为纤程(Fiber),或者绿色线程(GreenThread)。其实最直观的解释可以定义为线程的线程。有点拗口,但本质上就是这样。
线程的定义
        操作系统产生一个进程,进程再产生若干个线程并行的处理逻辑,线程的切换由操作系统负责调度。传统语言C++ Java等线程其实 与操作系统线程是1:1的关系,每个线程都有自己的Stack,Java在64位系统 默认Stack大小是1024KB,所以指望一个进程开启上万个线程是不现实的。但是实际上我们也不会这么干,因为起这么多线程并不能充分的利用CPU,大部分线程处于等待状态,CPU也没有这么核让线程使用。所以一般线程数目都是CPU的核数。
传统的J2EE系统都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,比如JDBC是同步阻塞的,这也是为什么很多人都说 数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。暴殄天物啊。另外过多的线程,也会带来更多的ContextSwitch开销。
Java的JDK里有封装很好的ThreadPool,可以用来管理大量的线程生命周期,但是本质上还是不能很好的解决线程数量的问题,以及线程空转占用CPU资源的问题。
现阶段行业里的比较流行的解决方案之一就是 单线程加上异步回调。其代表派是以及Java里的新秀。他们的核心思想是一样的,遇到需要进行I/O操作的地方,就直接让出CPU资源,然后注册一个回调函数,其他逻辑则继续往下走,I/O结束后带着结果向事件队列里插入执行结果,然后由事件调度器调度回调函数,传入结果。这时候执行的地方可能就不是你原来的代码区块了,具体表现在代码层面上,你会发现你的局部变量全部丢失,毕竟相关的栈已经被覆盖了,所以为了保存之前的栈上数据,你要么选择带着一起放入回调函数里,要么就不停的嵌套,从而引起反人类的Callback hell。
因此相关的Promise,CompletableFuture等技术都是为解决相关的问题而产生的。但是本质上还是不能解决业务逻辑的割裂。
说了这么多,终于可以提一下协程了,协程的本质上其实还是和上面的方法一样,只不过他的核心点在于 调度那块由他来负责解决,遇到阻塞操作,立刻yield掉,并且记录当前栈上的数据,阻塞完后立刻再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。比如Golang里的 go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。而这一切都是发生的用户态上,没有发生在内核态上,也就是说没有ContextSwitch上的开销。
既然我们的标题叫Java里的协程,自然我们会讨论JVM上的实现,JVM上早期有kilim以及现在比较成熟的Quasar。而本文章会全部基于Quasar,因为kilim已经很久不更新了。


为什么协程在Java里一直那么小众

其实早在JDK1的时代,Java的线程被称为GreenThread,那个时候就已经有了Fiber,但是当时不能与操作系统实现N:M绑定,所以放弃了。现在Quasar凭借ForkJoinPool这个成熟的线程调度库。另外,如果你希望你的代码能够跑在Fiber里面,需要一个很大的前提条件,那就是你所有的库,必须是异步无阻塞的,也就说必须类似于上的库,所有的逻辑都是异步回调,而自Java里基本上所有的库都是同步阻塞的,很少见到异步无阻塞的。而且得益于J2EE,以及Java上的三大框架(SSH)*,大部分Java程序员都已经习惯了基于线程,线性的完成一个业务逻辑,很难让他们接受一种将逻辑割裂的异步编程模型。
但是随着异步无阻塞这股风气起来,以及相关的coroutine语言Golang大力推广,人们越来越知道如何更好的榨干CPU性能(让CPU避免不必要的等待,减少上下文切换),阻塞的行为基本发生在I/O上,如果能有一个库能把所有的I/O行为都包装成异步阻塞的话,那么Quasar就会有用武之地,JVM上公认的是异步网络通信库是Netty,通过Netty基本解决了网络I/O问题,另外还有一个是文件I/O,而这个JDK7提供的NIO2就可以满足,通过AsynchronousFileChannel即可。剩下的就是如何将他们封装成更友好的API了。目前能达到生产级别的这种异步工具库,JVM上只有Vert.x3,封装了Netty4,封装了AsynchronousFileChannel,而且官方也出了一个相对应的封装了Quasar的库vertx-sync。
Quasar目前是由一家商业公司Parallel Universe控制着,且有自己的一套体系,包括Quasar-actor,Quasar-galaxy等各个模块,但是Quasar-core是开源的,此外Quasar自己也通过Fiber封装了很多的第三方库,目前全都在comsat这个项目里。随便找一个项目看看,你会发现其实通过Quasar的Fiber去封装第三方的同步库还是很简单的。

写在最后

异步无阻塞的编码方式其实有很多种实现,比如的提倡的Promise,对应到Java8的就是CompletableFuture。
另外事件响应式也算是一个比较流行的做法,比如ReactiveX系列,RxJava、Rxjs、RxSwift等。我个人觉得RxJava是一个非常好的函数式响应实现(JDK9会有对应的JDK实现),但是我们不能要求所有的程序员一眼就提炼出业务里的functor,monad(这些能力需要长期浸淫在函数式编程思想里),反而RxJava特别适合用在前端与用户交互的部分,因为用户的点击滑动行为是一个个真实的事件流,这也是为什么RxJava在 Android端非常火的原因,而后端基本上都是通过Rest请求过来,每一个请求其实已经限定了业务范围,不会再有复杂的事件逻辑,所以基本上RxJava在这端只是做了一堆的flatmap,再加上微服务化,所有的业务逻辑都已经做了最小的边界,所以顺序的同步的编码方式更适合写业务逻辑的后端程序员。
所以这里Golang开了个好头,但是Golang也有其自身的限制,比如不支持泛型,当然这个仁者见仁智者见智了,包的依赖管理比较弱,此外Golang没有线程池的概念,如果coroutine里的逻辑发生了阻塞,那么整个程序会hang死。而这点提供了一个Worker Pool的概念,可以将需要耗时执行的逻辑包到线程池里面,执行完后异步返回给EventLoop线程。
下一篇我们来研究一下vertx-sync,让里所有的异步编码方式同步化,彻底解决里的Callback Hell。