阻塞与非阻塞
- 主要是指,操作系统,或者底层的c库,提供的方法,或者是一个系统调用。也就是说我们调用这个方法的时候,这个方法可能会导致我的进程进入sleep状态,为什么会进入sleep状态呢?就是当前的条件不满足的情况下,操作系统主动的把我的进程切换为另外一个进程了,在使用当前的CPU。那么这样就是一个阻塞方法。
- 而非阻塞方法就是我们调用该方法,永远不会因为,当我们时间片未用完时,把我们的进程主动切换掉。
同步与异步
而同步和异步则是从我们调用的方式而言,就是我们编码中写我们的业务逻辑这样的一个角度。我们可以从nginx的发展历史趋势上可以看出这一点。那么nginx目前除了官方在提供的javascript利用这样的同步写代码的方式实现非阻塞编码的效果,以及openresty基于lua语言用同步写代码的方式实现非阻塞高并发的一个效果。
接下来,我们来看一看非阻塞和阻塞以及同步异步,到底有一些什么样具体的区别。
那么,在阻塞调用中,我们以accept为例。因为绝大多数程序,在调用accept的时候,它都是在使用阻塞socket的。使用阻塞socket的时候呢,当我们调用accept方法的时候,如果说我们监听的端口所对应的accept队列,就是操作系统已经为我们做好了几个三次握手建立成功的socket,那么阻塞方法可能会立刻得到返回,而不会被阻塞。但是,如果accept队列是空的,那么操作系统就会去等待新的三次握手的连接,到达我们的内核中,我们才会去唤醒这个accept调用,这个时间往往是可控的。我们去设置这个阻塞socket最长的超时时间,那么如果没有达到的话,也可以唤醒我们的这样一个调用。所以,这里的流程中,就是会产生进程间的主动切换。而我们之前谈过,像nginx是不能容忍这样的进程间切换的。因为它并发的连接实在是太多了。
那么非阻塞调用,有什么差别呢?我们先看accept如果你使用了非阻塞套接字,使用accept调用去执行的时候呢,如果accept队列为空,它是不等待立刻返回的。但是它返回的是什么呢?其实是EAGAIN,是一个错误码。所以这个时候呢,我们的代码回收一个错误码,但这个错误码是一个特殊的错误码。需要我们的代码去处理它。如果我们再次调用accept是非阻塞的,如果accept队列不为空的话,则把成功的那一个socket建立好的套接字返回给我们的代码。所以,这里有一个很大的问题,就是由我们的代码决定当accept收到一个EAGAIN这样的错误码时,我们究竟是应该等一会,继续处理这个连接,别不sleep一下,还是先切换到其他的任务,再处理。
我这里举的是一个非常简单的accept的例子。如果涉及到我们的业务特性,比如http的复杂的子请求、主请求,等等,实际上是会导致我们的代码非常复杂。因此,非阻塞调用呢,是我们的底层实现,如果我们用异步方法去使用非阻塞调用是非常自然而然的。我们可以看一下,是怎样用异步的方式去处理非阻塞连接。
那么我这里举了一个例子。是一个反向代理的例子,那么nginx做反向代理的时候有一个特点,他会去考虑到上游服务的处理能力相对是不足的,所以,如果是一个有body的http请求,那么,nginx会先把body接收完,再去向上游服务器发起连接,那么我们看右边这段代码。我们可以看到,当我们收完header的时候,我们已经知道,接下来向谁,哪一台服务器去发起反向代理,建立连接了。但是我需要先读取body,所以它调用了这样一个方法,那么这个方法就是一个标准的异步方法。它表达当我执行完read request body之后,再去调用post_handler方法,也就是upstream_init,是我们对上游服务器建立连接的方法,所以当我们调用这样的一个异步调用的时候它其实意味着先把body收完,再调这个方法。非常的复杂。
这是标准的异步调用
1 | rc = ngx_http_read_request_body(r, ngx_http_upsream_init); |
这个方法执行完时调用post_handler异步方法
1 | ngx_init_t |
最终读取完body后调用ngx_http_upstream_init方法
1 | void ngx_http_upstream_init(ngx_http_request_t *r) {} |
我们再来看一看,与此相反的同步调用的方法。比如说openresty写一段lua代码,比如说我现在要对redis建立连接,因为我们建立连接使用的也是TCP,那么TCP一样有三次握手,三次握手基于这种报文,那么我们在基于nginx openresty上也是不能使用阻塞方法的。但是如果你用异步的方式,非常复杂。同步方式呢,可以看到。
1 | local cliend = redis:new() |
我们new一个client redis以后,设置好这个连接的超时时间,我们就可以调用connect,这个connect就是一个同步调用,但是它里面走的是非阻塞代码。所以我们在写lua代码的时候,完全可以不必考虑像刚刚我们举的这个例子一样,考虑connect完成以后,再去回调另外一个方法,在另外一个方法中决定,connect是成功了还是失败了。如果成功了,我应该发消息,完全不需要这么做。我们只需要简单的connect,我收到响应值了,那么ok,如果是没有收到ok,那么我们打印一个ngx.say('failed')就可以了。因为在connect方法执行的过程中,当connect没有被满足,也就是我没有收到redis发来的ACK响应,就是成功建立连接时呢。这个connect方法不会返回,但是也不会阻塞nginx的代码,这就叫做同步调用代码。那么它使用了非阻塞的一个方式。
谈完同步异步阻塞与非阻塞以后我,我相信大家对于如何兼顾开发效率与我们的运行效率应该有了一个很好的认识。实际上,如果我们不是在极端场景下,都会去使用如openresty或者nginx的javascript模块,来使用同步编程的方式,达成我们的目的,这样的开发效率非常的高。