如何进行Golang和JVM中并发模型实现的探讨

今天就跟大家聊聊有关如何进行Golang和JVM中并发模型实现的探讨,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

说起来貌似有好久没有更新过博客了,主要是因为最近一段时间都在各种看书和看源码,所做的记录大部分也都是属于读书笔记性质,所以就没有整理到博客上来,之后会陆续整理一些东西上来。

引子

话说最近终于决定把之前收藏了很久的mit-6.824课程的lab拿出来做一下,这门课最有价值的地方就在于它设计了一些列的lab,让你能够在一定程度可控的工作量的coding之后比较深入地体会到众多分布式程序中所面临的一些列公共的问题以及如何去解决它们。例如,分布式容错、并发、网络底层实现等等。这门课的targeted language是golang。原因自然不说,因为golang的简洁所以非常适合用来替代C++等语言来作为lab的实现语言。

在实现的过程当中,我遇到的一个最主要的问题就是,如何在CPU密集型任务、I/O密集型任务以及充分利用多核CPU提升程序性能上找到一个平衡点。当然,这之中最容易想到的解决方案就是多线程。但是由于分布式程序的特殊性,它可能拥有大量的网络I/O或者计算任务。这就不可避免需要将使用同步的方式来抒写异步的情怀,解决方案就是将这些计算或者IO放到新的线程中去做,具体的线程调度交给操作系统来完成(虽然我们可以使用异步IO,但是异步IO由于存在大量的callback,不便于程序逻辑组织,所以这里不考虑直接使用异步IO)。这样有一个问题就在于,这之中会有大量的线程在context中,所以线程的上下文切换的开销不可忽视。如果我们在jvm中实现的话,大量的thread可能会很快耗尽jvm堆的内存,不仅会造成堆溢出,而且增大GC时间和不稳定性。因此,最近我就考察了几种常见的并发编程模型以及其对应常见的实现方式。

常见并发编程模型分类

并发编程模型,顾名思义就是为了解决高并发充分利用多核特性减少CPU等待提高吞吐量而提出的相关的编程范式。目前为止,我觉得比较常见的并发编程模型大致可以分为两类:

  • 基于消息(事件)的活动对象

  • 基于CSP模型的协程的实现

是的,貌似有大神已经做过了,学术界太可怕了!

首先第一个问题,当M发现在P中的gorouine链表已经全部执行完毕时,将会从其他的P中偷取goroutine然后执行,其策略就是一个工作密取的机制。当其他的P也没有可执行的goroutine时,就会从全局等待队列中寻找runnable的goroutine进行执行,如果还找不到,则M让出CPU调度。

第二个问题,例如阻塞IO读取本地文件,此时调用会systemcall会陷入内核,不可避免地会使得调用线程阻塞,因此这里goroutine的做法是将所有可能阻塞的系统调用均封装为gorouine友好的接口。具体做法为,在每次进行系统调用之前,从一个线程池从获取一个OS Thread并执行该系统调用,而本来运行的gorouine则将自己的状态改为Gwaiting,并将控制权交给scheduler继续调度,系统调用的返回通过channel进行同步即可。因此,这里其实goroutine也没有办法做到完全的协程化,因为系统调用总会阻塞线程。具体可以参考stackoverflow上的讨论:链接

第三个问题,go支持简单的抢占式调度,在goruntime中有一个sysmon线程,负责检测goruntime的各种状态。sysmon其中一项职责就是检测是否有长时间占用CPU的goroutine,如果发现了就将其抢占过来。

JDK上无法实现goroutine的原因

到这里,我们已经大致了解了goroutine的原理,可见goroutine中最重要的一个设计就在于它将所有的语言层次上的api都限制在了goroutine这一层,进而屏蔽了执行代码与具体线程交互的机会。所以在goroutine中,我们实际上就可以忽略线程的存在,把goroutine当成是一个非常廉价能够大量创建的Thread。

然而在Java中或者说打算和JDK交互的JVM系语言(如scala,clojure),本质上都无法完全实现goroutine(clojure虽然有async,但是依然无法和JDK中的阻塞api结合良好)。假设,我们在Java中基于Thread之上实现一个scheduler,一个轻量级的协程以及协程相关的原语(如resume, pause等),我们也只能基于我们自己封装的api来协助协程调度。如果在创建的协程中直接使用Java阻塞api,结果就是使得用来调度协程的OS Thread陷入阻塞,无法继续运行scheduler进行调度。

如何进行Golang和JVM中并发模型实现的探讨