Go语言出现后,Java还是好选择吗?
我们不妨看一个阻塞的系统调用futex的热点分布: 可以看到上面的热点中有大量涉及调度的开销。我们来看过程:
因此,上面2个误区与多线程的开销都有一定因果关系,但是真正的开销来源于线程阻塞唤醒调度。 综上,希望通过线程模型来提升web server性能的原则是:
文章后续将紧紧围绕这两个主题。 为了满足上述两个条件,使用eventloop+异步callback的方式是一个极佳的选择。 异步与协程的关系 为了保持简洁,我们以一个异步服务器上的Netty写操作为例子(写操作也存在阻塞的可能):
这里的sync()会阻塞线程。不满足期望。由于netty本身是一个异步框架,我们引入回调:
注意这里异步的write调用后,writeQuery会返回。因此假如逻辑上要求在write后执行的代码,必须出现在回调里,write是函数的最后一行。这里是最简单的情形,如果函数有其他调用者,那么就需要用CPS变换。 需要不断的提取程序的"下半部分",即continuation,似乎对我们造成一些心智负担了。这里我们引入kotlin协程帮助我们简化程序:
这里引入了一个魔法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来写回响应。 (编辑:广州站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |