2021-09-13
作者:互联网
导读 / Introduction
本文是今年QCon java专场《Java协程在腾讯的生产实践》主题分享,分享团队为腾讯大数据JVM团队。本文主要介绍协程的产生背景、java协程的发展历程、社区官方协程Project Loom的设计与实现,以及腾讯自研协程Kona Fiber的产生背景、设计与实现、性能测试和业务实践。
1. 协程产生的背景
1.1 线程模型
最经典的编程模型是线程模型,它是操作系统层面对cpu的抽象。由于线程模型是一种同步编程模型,它直观、易于理解,因此使用线程模型的开发效率高。但是对于IO密集型的程序,由于每次IO操作都需要Blocking当前线程,因此会产生一次线程切换,线程切换需要在用户态和内核态之间切换,且线程切换需要保存当前线程的执行上下文,一次线程切换的开销在10微秒量级。因此,对于IO密集型程序,cpu很大一部分都用于线程切换,导致cpu的利用率不高。
线程模型的第二个问题是,对于IO密集且高并发的程序,如果不采用异步编程模型,通常是一个线程对应一个并发(因为假设一个线程去做数据库访问操作,线程就被阻塞了,就不能执行其他任务了,只能用另一个线程去执行)。因此对于高并发的程序来说,需要创建大量线程。操作系统为了兼容各种各样的编程语言、执行逻辑,因此操作系统线程预留的栈内存通常较大,一般是8M。由于线程占用的内存较大,即使不考虑cpu利用率的问题,一台机器也很难创建太多线程(只考虑线程栈的内存开销,1万个线程就需要80G内存),因此很难在不使用异步编程框架的情况下,仅靠线程模型支持IO密集型+高并发程序。
1.2 异步模型
异步编程模型是一种编程语言框架的抽象,它可以弥补线程模型对于IO密集型+高并发程序支持的短板。它通过复用一个线程,例如线程在做io操作导致阻塞之前,通过回调函数调用到另一个逻辑单元,完成类似线程切换的操作;异步模型的执行效率很高,因为相比线程切换,它直接调用了一个回调函数;但是它的开发门槛较高,需要程序员自己理解哪里可能产生io操作,哪里需要调用回调函数,如何定期检查io操作是否做完;另外,由于线程的调用栈由一些逻辑上并不相关的模块组成,因此一旦出现crash之类的问题,调用栈比较难以理解,维护成本也较高。
1.3 协程
图1.1展示了线程模型和异步模型在生产效率和执行效率的对比图。可以看到,线程模型的生产效率是最高的,同时它对于IO密集型+高并发程序的执行效率较差。异步模型正好相反,它的生产效率较差,但是如果实现的很完美,它的执行效率是最高的。
协程的出现,是为了平衡线程模型和异步模型的生产效率和执行效率;首先,协程可以让程序员按照线程模型去编写同步代码,同时,尽可能降低线程切换的开销。
图1.1
2. Java协程的发展历程
上面分析了协程产生的背景,那么Java协程的现状是怎样的呢?
首先,由于Java生态丰富的异步框架,缓解了协程的紧迫性,用户可以用异步框架去解决IO密集型+高并发的程序。
如图2.1所示,列出了Java协程的发展历程。
图2.1
2.1 JKU
Java协程最早是JKU发表的论文+提供的patch。JKU的协程是一种有栈的协程,即每个协程都有自己独立的调用栈。不同于操作系统线程,由于JKU的协程只需要考虑java代码的执行,根据java代码执行时的特点,通常只需要较小的栈就可以满足需要,通常JKU的协程只需要不超过256K的栈。
2.2 Quasar/Kotlin
Quasar和Kotlin是之后出现的方案,它们都不需要修改java虚拟机,只需要修改上层java代码。它们都是无栈协程,所谓无栈协程,是指协程在suspend状态时不需要保存调用栈。那么如果协程切换出去以后,没有保存调用栈,下次恢复执行时,如何读取调用关系呢?Quasar和Kotlin在切换时,会回溯当前协程的调用栈,然后根据这个调用栈生成一个状态机,下次恢复执行时,根据这个状态机恢复执行状态;无栈协程通常不能在任意点切换,只能在被标记的函数切换,因为只有被标记的函数才能生成对应的状态机,Quasar需要加一个@Suspendable的annotation标记可以切换的函数,Kotlin需要在函数定义时加上suspend关键字标记可以切换的函数。
2.3 Project Loom
Project Loom是Openjdk社区推出的官方协程实现,它从立项到今天已经超过3.5年,目前已经包含27个Committer,超过180个author,3200+commits。
图2.1列出了Loom一些重要的时间点。Loom在17年底立项,18年初正式对外推出。19年7月,它发布了第一个EA(Early Access)Build,这时候它的实现还是一个Fiber类。在19年10月的时候,它的接口发生了一个较大的变动,将Fiber类去掉,新增了VirtualThread类,作为Thread类的子类,兼容所有Thread的操作。这时候它的基本思想已经比较清晰了,就是协程是线程的一个子类,支持线程的所有操作,用户可以完全按照线程的方式使用协程。
Loom作为Openjdk的官方实现,它的目标是提供一个Java协程的系统解决方案,兼容已有Java规范、JVM特性(例如ZGC、jvmti、jfr等),最终目标是对整个Java生态的全面兼容。对Java生态的全面兼容既是Loom的优势,同时也是它的挑战。因为Java已经发展了20多年,Loom要在最底层新加入一个协程的概念,需要适配的东西非常多,例如庞大而复杂的标准类库,大量JVM特性。所以,截至目前,Loom仍然有非常多的事情要做,目前还没有达到一个真正可用的状态,图2.1最右边的部分是我们的一个推测,我们猜测到jdk18或jdk19的时候,Loom或许可以加上一个Experimental标记,提供给用户使用。
3. Loom的实现架构
图3.1列出了Loom引入的一些新的概念,其中最重要的概念就是Virtual Thread,也就是协程。Loom的官方表述为:“Virtual threads are just threads that are scheduled by the Java virtual machine rather than the operating system”。站在用户的角度,可以把协程理解为线程,按照线程的方式使用,这也是Loom最重要的设计。
图3.1
除了Virtual Thread以外,Loom还新增了Scope Variable和Structured Concurrency的概念,后文会对它们分别进行介绍。
3.1 Loom的基本原理
如图3.2所示,展示了一个协程的生命周期。最初,执行VirtualThread.start()方法创建一个协程,等待被调度;当协程被调度执行以后,开始执行用户指定的业务代码,在执行代码的过程中可能会去访问数据库/访问网络,这些IO操作最后都会调用到底层的一个Park操作。Park可以理解为协程让出执行权限,且当前不能被调度执行。IO结束后会调用Unpark,Unpark之后协程可以被调度执行。在Park操作时,需要执行一个freeze操作,这个操作主要是将当前协程的执行状态,也就是调用栈保存起来。当协程Unpark且被调度时,会执行一个thaw操作,它是freeze的对称操作,主要是把之前freeze保存的调用栈恢复到执行线程上。
图3.2
图3.3展示了freeze和thaw具体完成的内容。首先看freeze操作,调用栈的上半部分从ForkJoinWorkerThread.run到Continuation.enterSpecial都是类库里面的调用。从A开始才是用户的业务代码,假设用户调用了函数A,函数A又调用了函数B,函数B又调用了函数C,之后函数C有一个数据库访问操作,导致协程让出执行权限(执行了一个yield操作),接下来调用到freeze保存当前协程的执行状态(调用栈);这时会首先产生一个stack walk的动作,Loom会从当前调用栈的最底层逐步向上遍历,一直遍历到Continuation.enterSpecial为止。将所有遍历到的栈中的oop都保存一个refStack中,这样做可以保证协程的栈在GC时不作为root。
通常,thread的栈都会被GC当作root来处理,且处理root时通常是需要stop-the-world的,如果协程的栈和线程的栈一样,也被当作root处理,那么由于协程可以支持到几百万甚至上千万的量级,这会导致stop-the-world的时间变长。因此stack walk和refStack的设计(将协程栈上的内容拷贝到一个refStack中),可以在concurrent阶段处理协程的栈,不会由于协程数量的增多影响GC的暂停时间。
图3.3
另一方面,每次freeze时,Loom都会将协程当前的执行栈拷贝到java heap中,即本例中的A、B、C和yield被单独拷贝出来,这样做可以保证协程的栈的内存真正做到按需使用。
当协程被Unpark且被调度时,协程执行thaw操作。thaw主要是将之前保存在java heap里的协程栈恢复到执行栈上,Loom在这里引入了lazy copy的优化。所谓lazy copy,是指Loom通过大量的profiling,发现大部分程序在io操作以后,还会紧跟着产生又一次io。具体到当前的例子,函数C在触发一次io操作以后,很可能还会产生一次io操作,而不是执行完毕返回到函数B,然后函数B也执行完毕返回到函数A。这样的话,在每次thaw的时候,就没必要把所有的调用栈都拷贝回去,而只需要拷贝一部分,然后在拷贝的调用栈尾部加一个return barrier,如果函数确实返回到return barrier,可以通过return barrier触发继续拷贝调用栈的动作;这样每次freeze和thaw的时候,都只需要拷贝一小部分内容,大大提升了切换性能。
在freeze和thaw的时候间隔里,有可能触发过GC导致oop被relocate。因此thaw的时候需要执行一个restore oop的动作,确保不会出现内存访问异常。
3.2 VirtualThread的使用
-
调度器:
协程可以理解为一种用户态线程,在用户态执行时,由于不能直接访问CPU,所以只能用线程代替cpu。所以,协程需要一个用户态的调度器,调度器中包含物理线程,协程被调度器放在物理线程上执行。如果用户不指定调度器,默认调度器是ForkJoinPool,Loom针对ForkJoinPool做了很多关于协程调度的优化。
-
直接创建VirtualThread:
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join()
相比于创建Thread,只需要增加一个ofVirtual()的函数调用,创建的就是协程。
如果用户想要自己写一个针对自己业务模型更有效的调度器,可以通过调用scheduler()函数指定调度器,代码如下所示:
Thread thread = Thread.ofVirtual().scheduler(CUSTOM_SCHEDULER).start(() -> System.out.println("Hello"));
thread.join()
- 创建协程池
除了直接使用协程以外,还可以通过创建协程池的方式使用协程,例如创建一个包含10000个协程的协程池,将任务提交给协程池执行。对应代码如下:
ThreadFactory factory
if (UseFiber == false) {
factory = Thread.ofPlatform().factory();
} else {
factory = Thread.ofVirtual().factory();
}
ExecutorService e = Executors.newFixedThreadPool(ThreadCount, factory);
在创建ThreadPool时,如果传入的factory是ofPlatform()的factory,对应的就是线程池,如果是ofVirtual()的factory,对应的就是协程池。
3.3 VirtualThread Pin
Pin的状态指的是VirtualThread在freeze时无法让出Carrier Thread(协程执行时挂载的物理线程)。主要有两种情况下会导致Pin:
-
VirtualThread的调用栈包含JNI frame。因为JNI调用的实现是C++代码,可以做的事情非常多,例如它可以保存当前Carrier Thread的Thread ID,如果这时切换出去,那么下一次执行时,如果另一个Carrier Thread来执行这个协程,将会产生逻辑错误(Carrier Thread的ID不一致);
-
VirtualThread持有synchronized锁。这是java早期锁的实现带来的限制,因为java的synchronized锁的owner是当前的Carrier Thread,如果一个协程持有一把锁,但是锁的owner被认为是当前的Carrier Thread,那么如果接下来这个Carrier Thread又去执行另一个协程,可能另一个协程也被认为拥有了锁,这可能导致同步的语义发生混乱,产生各种错误。
偶尔出现的Pin并不是一个很严重的问题,只要调度器中始终有物理线程负责执行协程就可以。如果调度器中所有的物理线程都被Pin住,可能会对吞吐量产生较大影响。Loom针对默认的调度器ForkJoinPool做了优化,如果发现所有的物理线程都被Pin住,就会额外创建一些物理线程,保证协程的执行不受太大影响。用户如果想要彻底消除Pin,可以按照如图3.4的方式,通过-Djdk.tracePinnedThreads选项定位产生Pin的调用栈。
图3.4
3.4 Structured Concurrency
结构化并发的设计初衷是方便管理协程的生命周期。结构化并发的基本想法是,调用一个method A,通常并不关心method A是由一个协程按部就班的执行,还是将method A划分为100个子任务,由100个协程同时执行。下面的代码是使用结构化并发的一个小例子:
ThreadFactory factory = Thread.ofVirtual().factory();
try (ExecutorService executor = Executors.newThreadExecutor(factory)) {
executor.submit(task1);
executor.submit(task2);
}
当前执行的协程会在try代码段结束的位置等待,直到try对应的代码段执行结束,就好像try代码段中的内容是由当前协程按部就班的执行一样。结构化并发本质上是一种语法糖,方便将一个大任务划分为多个小任务,由多个协程同时执行。有了结构化并发,很自然的会产生结构化的中断和结构化的异常处理。结构化的中断,是指try代码段的超时管理,例如用户想要try中的task1和task2在30秒内完成,如果30秒内没有完成则产生一个中断,结束执行。结构化的异常处理,是指try中的内容被划分为2个子任务,如果子任务产生异常,对于异常处理来说,可以做到和一个协程顺序执行的做法相同。
3.5 Scope Variable
Scope Variable是Loom社区增加协程以后,对原有ThreadLocal的重新思考。Scope Variable可以理解为轻量的、结构化的Thread Local。由于Thread Local是全局有效的、非结构化的数据,因此一旦修改就会覆盖掉之前的值。Scope Variable是一种结构化的Thread Local,它的作用范围仅限一个Code Blob,下面的代码是Scope Variable的一个测试用例,根据assert信息可以看出Scope Variable如果在Code Blob之外就会自动失效。
public void testRunWithBinding6() {
ScopeLocal<String> name = ScopeLocal.inheritableForType(String.class);
ScopeLocal.where(name, "fred", () -> {
assertTrue(name.isBound());
assertTrue("fred".equals(name.get()));
ScopeLocal.where(name, "joe", () -> {
assertTrue(name.isBound());
assertTrue("joe".equals(name.get()));
ensureInherited(name);
});
assertTrue(name.isBound());
assertTrue("fred".equals(name.get()));
ensureInherited(name);
});
}
Scope Variable的另一个好处是,它可以和结构化并发巧妙结合起来。结构化并发通常会将一个大任务划分为多个子任务,如果子任务的个数非常多,例如一个大任务被划分成1000个子任务,那么如果采用Inherit Thread Local来复制父协程的Thread Local到子协程上,由于Thread Local是mutable的,所以子协程也只能拷贝父协程的Thread Local。在子协程非常多的情况下,这种拷贝开销很大。Scope Variable为了应对这种情况,仅仅在子协程上增加一个父协程的引用,而不需要额外的拷贝开销。
4. 为什么需要Kona Fiber?
通过前面的分析可以看出,Loom的设计是非常完善的,对各种情况都有充分的考虑,用户只需要等待Loom成熟以后使用Loom即可。那么,为什么腾讯还需要自研一个协程Kona Fiber呢?
我们通过和大量业务的沟通,分析出当前业务对协程的三个主要需求:
-
在JDK8/JDK11上可用:当前大量业务还是基于JDK8/JDK11进行开发的,而Loom作为Openjdk社区的前沿特性,基于社区的最前沿版本进行开发,让很多还在使用旧版本JDK的业务无法使用;
-
代码的可演进性:用户希望基于JDK8/JDK11修改的协程代码,未来升级到社区最新版本的时候,能够在不修改代码的情况,切换到社区官方的协程Loom;
-
切换性能的需求:当前Loom的实现,由于有stack walk和stack copy的操作,导致切换效率还有一定提升空间,用户希望有更好的切换效率。
基于这三点需求,我们设计并实现了Kona Fiber。
5. Kona Fiber的实现
5.1 Kona Fiber与Loom的异同
图5.1展示了Kona Fiber和Loom的共性以及差异点,中间黄色的部分是Kona Fiber和Loom都支持的,两边的蓝色部分是Kona Fiber和Loom的差异点。
图5.1
首先看共性的部分:
-
Loom最重要的设计是VirtualThread,对于Virtual Thread的接口,KonaFiber是全部支持的,用户可以用相同的一套接口使用Loom和Kona Fiber。
-
Loom针对ForkJoinPool做了很多优化,包括前面提到的自动扩展Carrier Thread,Kona Fiber也对这一部分优化进行了移植和适配。
-
对于Test Case,由于Kona Fiber的接口与Loom接口的一致性,理想情况下可以不加任何修改,直接运行Loom的Test Case。当然,实际运行Loom的Test Case时,仍然需要少量修改,这些修改主要是由于大版本的差异。因为Loom是基于最新版本(jdk18),而Kona Fiber是基于jdk8,如果Loom的Test Case包含了jdk8不支持的特性,例如jdk8不支持var的变量定义,那么仍需要少量的适配。当前,Loom的大部分Test Case我们已经移植到了Kona Fiber,对应的文件目录(相对于jdk的根目录)为jdk/test/java/lang/VirtualThread/loom。
对于差异的部分,首先是性能方面,由于Kona Fiber是stackful的方案,在切换性能上会优于Loom,在内存开销上也会比Loom多一些,这部分的详细数据会在下一章进行介绍。其次,由于Loom引入了一些新的概念,这些概念虽然可以让程序员更好的使用协程,但是这些概念的成熟以及被程序员广泛接受,都需要一定的时间。未来如果用户对Scope Variable、Structure Concurrency有共性需求,Kona Fiber也会虑引入这些概念,目前还是以开箱即用为目标,暂不支持这些新特性。
5.2 Kona Fiber的实现架构
如图5.2所示,展示了一个Kona Fiber协程的生命周期。
图5.2
第一个步骤和第二个步骤与Loom相同,即协程被创建、协程被调度执行。当协程真正被调度执行时,才会在runtime创建协程的数据结构以及协程的栈。创建成功以后,返回到java层执行用户代码。接下来如果在用户代码中遇到IO操作(例如数据库访问),会导致协程被Park,因此会进入到runtime,这时在runtime会执行一个stack switch的过程,切换到另一个协程执行。IO结束时(例如数据库访问完成),会唤醒协程继续执行。
图5.3
如图5.3所示,展示了Kona Fiber在stack switch时的具体实现。协程可以理解为用户态的线程,又因为Kona Fiber的每个协程都有一个独立的栈,所以协程切换本质上只需要切换rsp和rbp指针即可。因为,Kona Fiber的切换开销相比于Loom的stack walk和stack copy要小一些,在理论上会有一个更好的性能。接下来会有详细的数据对Kona Fiber和Loom进行比较。
6. Kona Fiber的性能数据
图6.1展示了Kona Fiber、Loom和JKU的切换性能数据,横轴代表协程个数,纵轴表示每秒切换的次数。
图6.1
可以看到,Kona Fiber的性能优于Loom,且当协程个数较多时,JKU的性能也好于Loom。前文叙述过,由于Loom在切换时需要做stack copy和stack walk,所以导致切换性能会差一点。
图6.2
图6.2展示了Kona Fiber、Loom和JKU在创建30000个协程时的内存开销,无论是runtime直接使用的物理内存,还是JavaHeap的内存使用,Loom都是最优的(占用内存最少),当然这也得益于Loom的stack copy的实现,可以做到对内存真正的按需使用。由于Kona Fiber兼容了Loom Pin的概念,因此相比JKU去掉了很多不需要的数据结构,在内存使用上优于JKU。从内存使用总量上,虽然Kona Fiber占用的内存多于Loom,但是30000个协程总体占用不到1G内存(runtime占用的内存加上Java Heap占用的内存),对于大多数业务也是可接受的。
图6.3
图6.4
图6.3、图6.4分别展示了Kona Fiber和Loom在使用默认调度器ForkJoinPool时的调度性能。其中横轴表示调度器中Carrier Thread的个数,纵轴表示调度器每秒完成切换操作的次数,不同颜色的线代表不同个数的协程。可以看到,Loom在协程个数较多时,性能抖动较明显。Kona Fiber在不同协程个数的情况下,性能表现都很稳定。这种差异的产生,可能还是与Loom在切换时要做stack walk和stack copy有关。
注:
1. 所有关于Loom的性能数据,都是基于2020年9月Loom的代码。
2. 所有的性能测试用例都可以在开源代码中获取,对应的目录(相对于jdk的根目录)为demo/fiber
7. Kona Fiber的业务落地
7.1 业务协程化改造
如果一个业务想要从线程切换到协程,通常需要以下三个步骤:
1.将创建线程改为创建协程;将线程池改为协程池。第一步非常简单,只需要将线程的使用替换为协程的使用(按照3.2小节“Virtual Thread的使用”进行替换即可)
2.将部分同步接口替换为异步框架。当前Kona Fiber仍有一些接口不支持,例如Socket、JDBC相关的接口。使用这些接口,将导致协程不能正常切换(Blocking在native代码或者Pin住),协程会退化成线程,那么协程的优势也就不存在了。目前Kona Fiber对网络和数据库的一些原生接口不支持,好在一般网络和数据库操作都可以找到对应的异步框架,例如网络操作有Netty,数据库有异步的redis。图7.1以替换Netty为例,介绍了如何利用异步框架有效的使用协程。首先,创建一个CompletableFuture,然后将任务提交给Netty,接下来当前协程调用Future.get()等待Netty执行完成。在Netty执行完成的回调函数里调用Future.complete(),这样协程就可以恢复执行。
图7.1
3. 协程性能优化:Pin的解决。3.3小节介绍过Pin,Pin虽然不会导致整个系统死锁,但是频繁的Pin仍然会显著降低业务的吞吐量。对于Pin的解决,主要是两个方面,即产生Pin的两个原因:synchronized锁和native frame。第一步,在调试阶段开启-Djdk.tracePinnedThreads,这样可以找到所有引起Pin的调用栈。如果Pin是由于业务代码使用synchronized锁引起的,那么只需要将synchronized锁替换成ReentrantLock即可;如果Pin是由于包含native frame或者第三方代码包含synchronized锁引起的,那么只能通过将任务提交到一个独立线程池的方法来解决,这样可以保证协程的执行不受影响。
7.2 企点开放平台-统一推送服务
企点开放平台-统一推送服务是腾讯公司的业务,天然存在高并发的需求。最初业务方尝试使用WebFlux响应式编程,但由于业务方存在较多第三方外包人员,且WebFlux的开发、维护难度较高,导致业务方放弃使用WebFlux。后来,在了解了腾讯内部自研的Kona Fiber后,业务方果断选择尝试切换Kona Fiber。
业务方针对Kona Fiber的适配,主要是通过nio+Future替换bio,将所有阻塞操作替换为nio,当阻塞操作完成时执行Future.complete()唤醒协程;业务方反馈的替换协程的工作量为:三人天、200+行的代码适配、测试工作。最终相比Servlet的线程方案,系统整体吞吐量提升了60%。
下面的代码是业务方修改的代码片段,分别表示创建一个协程池,以及通过加一个annotation让函数运行在协程池上:
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadFactory threadFactory = Thread.ofVirtual().factory();
return Executors.newFixedThreadPool(fiberCount, threadFactory);
}
@Async("asyncExecutor")
public CompletableFuture<ResponseEntity<ResponseDTO>> direct() {
···
}
7.3 SLG游戏后台服务
SLG(Simulation Game)游戏主要是一些策略类游戏,这类游戏不像一些对战类游戏对实时性要求非常高。策略类游戏的特点是逻辑复杂,且游戏业务通常都有高并发、高性能需求。业务方基于Kona Fiber定制了一种单并发的协程调度器。
如图7.2所示,多个协程只运行在一个playerService Thread上,由于Carrier Thread只有一个线程,因此同一时刻只有一个运行在playerService Thread的协程可以执行;这样做可以省去很多同步操作,提高开发者的编程效率。替换了Kona Fiber以后,系统的整体吞吐量也相比线程方案提高了35%。
图7.2
右侧的battleService Thread存在访问数据库请求,业务方使用的是腾讯自研的Tcaplus数据库,为了避免协程退化成线程,业务方将数据库操作提交到一个独立的线程池来执行。
7.4 trpc-java
trpc是腾讯公司自研的高性能、新老框架易互通、便于业务测试的RPC框架。一些使用trpc-java的用户对高并发+IO密集型程序有需求。在协程出现以前,他们都只能通过trpc-java提供的异步框架解决性能问题。异步框架虽然可以解决高并发+IO密集型程序的性能问题,但是由于它对开发者要求较高,很多时候用户一不小心就会将异步代码写成同步代码,导致性能下降。
当前,trpc-java已经结合Kona Fiber推出了trpc-java协程版本,用户可以按照编写同步代码的方式,获得异步代码的性能。目前已有大数据特征中台业务、trpc-网关业务正在适配协程。
8. Future Plan
-
Kona Fiber在Kona8上的源码github链接:
-
Kona Fiber在Kona11上的源码github链接:
-
持续跟进Loom社区,将Loom的优化移植到Kona Fiber。
参考文献
传送门
Kona 8对外开源版本,欢迎star:
https://github.com/Tencent/TencentKona-8
Kona11对外开源版本,欢迎star:
https://github.com/Tencent/TencentKona-11
- 标题图来源:Pexels -
扫码关注 | 即刻了解腾讯大数据技术动态
标签:13,Loom,协程,Thread,Kona,09,Fiber,线程,2021 来源: https://blog.csdn.net/Tencent_BigData/article/details/120262506