需求: 如何写个异步调用,以后能力高了,写出个异步框架呢?
今天我来谈谈自己的想法,也是为以后尝试写异步框架奠定些基础。
第一,异步是什么东西呢? 我的理解异步就是(1) 不等待,直接返回 (2)对于异步框架,比如vertx,一般会有 callback。(3)对于异步框架来说,主线程不会终止异步线程。(4) 对于单线程来说,比如js,会形成event queue
好了,其实掌握异步的线程模型,得好好掌握netty reactor线程模型。
第二, java 如何写个异步调用的? 听起来很简单,异步嘛,callback就可以了。一开始我确实是这么想的,因为我最近vertx 框架想多了,一个简简单单的callback 就实现了异步调用。 但是java的callback 却实现不了异步。至于js callback 异步,我们后面分析。那我们怎么写java 异步呢? 异步的本质是什么呢,为什么我写的callback代码是同步,别人写的callback代码就是异步呢? 首先得清楚的知道以下几点。
(1) 根据李林锋的文章,jdk 1.0 到jdk 1.3 , java 的 IO 类库都非常原始,很多unix 网络编程中的概念或者接口再IO类库中都没有体现,例如 Pipe, channel,Buffer,和 Selector。 jdk1.4 时候,出现了Nio 的包,提供了进行异步的API 和类库,比如 byteBuffer,Pipe, serverSocketChannel 和socketChannel,多路复用的selector。按照我的理解,这就表明,jvm 底层 再jdk 1.0 和 jdk 1.3 就不支持异步的操作。
(2) java 的主线程 和 子线程之间的关系。比如setDaemon(true),主线程会终止子线程,尽管子线程还没有执行完。 Thread.join(). 主线程会等待子线程执行完了之后,才会执行主线程。当我们什么都不设置的时候,主线程和子线程之间 是不互相影响的。vertx 就是这样的异步框架。vertx 的callback 都是一个个handler。
(3) 线程调用,底层由系统来决定的,比如java 里面的native 方法。linux fork 一个子线程和windows fork 子线程可能是不一样的。Nio 的线程模型是如何执行的,需要操作系统的线程调用。
那我要是想写个java 异步程序是不是很简单? 不用callback,直接 new thread(()-> {system.out,println("async invoking")}).start()。 Thread 里面的run 方法就会异步的执行. 请看下图, 是不是很简单? 但是我会问,为什么执行线程这块不是同步呢? 都由主线程来执行呢,可是当new 一个新的线程之后,就会告诉主线程,你继续执行把,新线程里面的东西,我自己的线程栈执行就可以了,你不用管我。这就是为什么scheduledthreadPool, fixedthreadPool,cachethreadPool等,这些都是异步的,由新的线程池来执行,主线程该干啥干啥去。这就是异步!至于为什么 新new thread就是异步的,我觉得这个得看背后的 jvm 怎么实现的,jvm 是如何调用操作系统的线程来做异步代码执行的。 new thread start方式调用的是native() 方法:
private native void start0();
第三: 那是不是 我就可以写异步框架,比如通信的server 端了? 不管是 grpc http2协议,vertx tcp 通信和http 协议。我们还得想想 性能问题,我可以用socket 通信简单写个异步的server 端出来,但是性能呢? 可以接多少个客户端的请求呢?
那我们再等等,需要了解下面的 线程模型的演变,note: 个人觉得底层的操作,比如多路复用,还是需要操作系统底层的支持的,现在是多核多CPU,多线程的 硬件设备。 但是底层到底是怎么支持,我确实不太清楚,但是不妨碍我们继续往下走。
(1).
传统的java BIO web 模型,比如tomcat, 一个请求就会由一个线程进行阻塞的处理。这样的线程模型支持的并发量并不是很高,二是线程阻塞,很容易导致服务端的crash 掉。 而且我们不能同时创建这么多线程来阻塞执行。
(2).
看下上面的图,是不是很明了? 由于(1) 图是来一个请求,就会创建一个新的线程,这个弊端很是明显,那么(2) 图,就使用一个线程池的方式来做,线程池控制总的线程数量。因此避免了每个请求都会创建一个新线程从而耗尽资源。 但是由于底层仍是采用了同步阻塞的方式处理,从而仍然不能解决性能, 可靠性, 可维护性等问题。
(3)
jdk 5之后,java 底层通信发生了变化,许多的Nio的出现,使得 java 线程 不再是以阻塞的方式调用,异步调用,多路复用的方式开始出现。我们看上面的线程模型,selector 多路复用的选择一个就绪的cahnnel,然后dispatcher 分发,给 reactor thread pool handler 异步执行。然后异步处理完成之后,再由selector 将结果通过channel 给客户端。但是问题是reactor thread pool 线程也会越积越多。我觉得这样的线程模型在有限的线程资源下,利用的越来越合理,但是真当许多的并发流量来的时候,还是要扩容机器的。
(4)
multiple reactor 模式, 有了mainReactor 和 subReactor, mainReactor 主要负责 accept, subReactor 则负责 read, send等操作。其中由read 等操作,再进一步给thread pool 来操作。 整个线程模型是越来越有效率。比最初的好处有哪些呢?
1. 每个client 请求 不是新new thread 线程来处理,而是有acceptor 通过多路复用 统一的处理。
2. task 处理,比如各种handler 不再同步的处理,而是采用异步的线程池的方式处理。此外各个线程池分工合作,高效很多。
但是此处,我有一点点不理解的地方,selector 的多路复用,应该只用在了acceptor 和 channel 的状态等, 真正的任务处理,也还是要线程池里面的线程去执行,然后通过回调返回给selector,返回给 channel。 这样不会有阻塞,线程池来管理,各个部分分工合作。
第四步: 好了,当我们大致了解这些东西之后,我们就可以选择一个框架来写异步框架了,当然首选的还是netty,grpc 使用的netty4.X 版本的,支持http2 的标准协议。这个框架怎么写,需要以后有时间尝试写个rpc 异步框架。但是呢,介绍到这里,我觉得应该初步明白应该从哪里研究哪里动手了。
最后我还想简单的介绍下 js 的 eventloop 和 vertx 的eventloop。
首先看js 的:
js是单线程的,HTML5提出Web Worker标准,但是子线程完全受主线程控制,且不得操作DOM。从而说js 还是单线程的。js 的eventloop 就非常的简单,不像java netty。
这个简单把,上面图来自与Philip Roberts的演讲《Help, I'm stuck in an event-loop》。
你看stack 线程栈按顺序执行着,有回调(callback),比如ajax, 我就把回调放到下面的callback queue 里面,然后等主线程栈的代码执行完了之后呢,就开始执行callback queue 里面的 onclick,onload 等等callback。 我个人觉得里面异步执行是有异步执行的线程的,将异步执行的结果放到queue 队列就结束了,因此我觉得主线程是eventloop线程,但是异步执行的线程也是不可避免的,不然谁来执行ajax 请求呢? 就像set timer 一样,我觉得还是有timer 这样的线程来执行的。
那vertx 呢? 这个vertx 线程模型也是由eventloop来的。当设置vertx.deploy(verticle).setWorker(true)的时候,在每个verticle里面的代码执行都是eventloop 主线程执行的,里面每个函数的callback,都是有子线程,也就是worker线程来执行的,但是verticle里面是没有race condition 关系的,也就是说同一时间,只会有一个线程执行代码,我也很好奇这种的执行方式是如何保证的。 我觉得是每个 callback 就是个callback, 然后像js 那样,再eventloop 主线程执行完之后,才从handler queue 取出线程,一个一个执行,此时执行的时候,就是worker 线程。
这些框架到处都是神奇的地方,如果想完全理解,只有自己写个异步框架就好理解了。不然也会许多不明白的地方,希望我有机会自己写个异步框架,这才是霸气所在把。另外有些地方还是值得以后慢慢研究,研究越深,就会有豁然开朗感觉。