加入收藏 | 设为首页 | 会员中心 | 我要投稿 广州站长网 (https://www.020zz.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 移动互联 > 评测 > 正文

Go语言出现后,Java还是好选择吗?

发布时间:2019-11-04 20:44:43 所属栏目:评测 来源:梁希
导读:副标题#e# 随着大量新生的异步框架和支持协程的语言(如Go)的出现,在很多场景下操作系统的线程调度成为了性能的瓶颈,Java也因此被质疑是否不再适应最新的云场景了。4年前,阿里JVM团队开始自研Wisp2,将Go语言的协程能力带入到Java世界。既享受Java的丰富

我们不妨看一个阻塞的系统调用futex的热点分布:

Go语言出现后,Java还是最佳选择吗?

可以看到上面的热点中有大量涉及调度的开销。我们来看过程:

  1. 调用系统调用(可能需要阻塞);
  2. 系统调用确实需要阻塞,kernel需要决定下一个被执行的线程(调度);
  3. 执行上下切换。

因此,上面2个误区与多线程的开销都有一定因果关系,但是真正的开销来源于线程阻塞唤醒调度。

综上,希望通过线程模型来提升web server性能的原则是:

  1. 活跃线程数约等于CPU个数
  2. 每个线程不太需要阻塞

文章后续将紧紧围绕这两个主题。

为了满足上述两个条件,使用eventloop+异步callback的方式是一个极佳的选择。

异步与协程的关系

为了保持简洁,我们以一个异步服务器上的Netty写操作为例子(写操作也存在阻塞的可能):

  1. private void writeQuery(Channel ch) { 
  2.   ch.write(Unpooled.wrappedBuffer("query".getBytes())).sync(); 
  3.   logger.info("write finish"); 

这里的sync()会阻塞线程。不满足期望。由于netty本身是一个异步框架,我们引入回调:

  1. private void writeQuery(Channel ch) { 
  2.   ch.write(Unpooled.wrappedBuffer("query".getBytes())) 
  3.     .addListener(f -> { 
  4.       logger.info("write finish"); 
  5.     }); 

注意这里异步的write调用后,writeQuery会返回。因此假如逻辑上要求在write后执行的代码,必须出现在回调里,write是函数的最后一行。这里是最简单的情形,如果函数有其他调用者,那么就需要用CPS变换。

需要不断的提取程序的"下半部分",即continuation,似乎对我们造成一些心智负担了。这里我们引入kotlin协程帮助我们简化程序:

  1. suspend fun Channel.aWrite(msg: Any): Int = 
  2.     suspendCoroutine { cont -> 
  3.         write(msg).addListener { cont.resume(0) } 
  4.     } 
  5.  
  6. suspend fun writeQuery(ch: Channel) { 
  7.     ch.aWrite(Unpooled.wrappedBuffer("query".toByteArray())) 
  8.     logger.info("write finish") 

这里引入了一个魔法suspendCoroutine,我们可以获得当前Continuation的引用,并执行一段代码,最后挂起当前协程。Continuation代表了当前计算的延续,通过Continuation.resume()我们可以恢复执行上下文。因此只需在写操作完成时回调cont.resume(0),我们又回到了suspendCoroutine处的执行状态(包括caller writeQuery),程序继续执行,代码返回,执行log。从writeQuery看我们用同步的写法完成了异步操作。当协程被suspendCoroutine切换走后,线程可以继续调度其他可以执行的协程来执行,因此不会真正阻塞,我们因此获得了性能提升。

从这里看,只需要我们有一个机制来保存/恢复执行上下文,并且在阻塞库函数里采用非阻塞+回调的方式让出/恢复协程,就可以使得以同步形式编写的程序达到和异步同样的效果了。

理论上只要有一个库包装了所有JDK阻塞方法,我们就可以畅快地编写异步程序了。改写的阻塞库函数本身需要足够地通用流行,才能被大部分程序使用起来。据我所知,vert.x的kotlin支持已经做了这样的封装。

虽然vert.x很流行,但是无法兼顾遗留代码以及代码中的锁阻塞等逻辑。因此不能算是最通用的选择。实际上Java程序有一个绕不过的库——JDK。Wisp就是在JDK里所有的阻塞调用出进行了非阻塞+事件恢复协程的方式支持了协程调度,在为用户带来最大便利的同时,兼顾了现有代码的兼容性。

上述方式支持了,每个线程不太需要阻塞,Wisp在Thread.start()处,将线程转成成了协程,来达到了另一目的: 活跃线程数约等于CPU个数。因此只需要使用Wisp协程,所有现有的Java多线程代码都可以获得异步的性能。

手工异步/Wisp性能比较

对于基于传统的编程模型的应用,考虑到逻辑清晰性、异常处理的便利性、现有库的兼容性,改造成异步成本巨大。使用Wisp相较于异步编程优势明显。

下面我们在只考虑性能的新应用的前提下分析技术的选择。

基于现有组件写新应用

如果要新写一个应用我们通常会依赖JDBC、Dubbo、Jedis这样的常用协议/组件,假如库的内部使用了阻塞形式,并且没有暴露回调接口,那么我们就没法基于这些库来写异步应用了(除非包装线程池,但是本末倒置了)。下面假设我们依赖的所有库都有回调支持,比如dubbo。

1)假设我们使用Netty接受请求,我们称之为入口eventLoop,收到请求可以在Netty的handler里处理,也可以为了io的实时性使用业务线程池。

2)假设请求处理期间需要调用dubbo,因为dubbo不是我们写的,因此内部有自己的Netty Eventloop,于是我们向dubbo内部的Netty eventLoop处理IO,等待后端响应后回调。

3)dubbo eventLoop收到响应后在eventloop或者callback线程池调用callback。

4)后续逻辑可以在callback线程池或者原业务线程池继续处理。

5)为了完成对客户端的响应最终总是要由入口的eventloop来写回响应。

(编辑:广州站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读