Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor

“阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,提供一个从分布式系统角度的回答。

阻塞与非阻塞

阻塞和非阻塞关注的是
程序在等待调用结果(消息,返回值)时的状态。是否会阻塞当前线程。

  • 阻塞:
    调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
    eg : 应用程序在获取网络数据的时候,如果网络传输数据很慢,那么程序就一直等着,知道传输完毕为止。
    int a = 1 +1;//此代码是阻塞的。

  • 非阻塞 :
    在不能立刻得到结果之前,该调用不会阻塞当前线程。

eg:
你打电话问书店老板有没有《分布式系统》这本书,
你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,
如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

链接:https://www.zhihu.com/question/19732473/answer/20851256

同步与异步:

第一种讲法:

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

举个通俗的例子:你打电话问书店老板有没有《分布式系统》这本书,
如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

第二种讲法:

同步:
应用程序(代码)会直接参与IO读写操作,并且我们的应用程序会直接阻塞到某一个方法上,直到数据准备就绪;或者采用轮询的策略实时检查数据的就绪状态,如果就绪则获取数据。

异步:
则所有的IO读写操作交给操作系统处理,与我们的应用程序没有直接关系,我们的程序不需要关系IO读写,当操作系统完成IO读写操作时,会给我们应用程序发送通知,我们的应用程序直接拿走数据即可。

简而言之:
同步像同步方法,此方法直接返回结果。
异步像异步方法回调,等待异步 callback。

同步说的是server服务器的执行方式;
阻塞说的是具体的技术,接受数据方式、状态(IO、NIO)

同步 /异步描述的是执行IO操作的主体是谁
同步是由用户进程自己去执行最终的IO操作。异步是用户进程自己不关系实际IO操作的过程,只需要由内核在IO完成后通知它既可,由内核进程来执行最终的IO操作。

第三种举例:

举例:
老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
3 老张把响水壶放到火上,立等水开。(异步阻塞)
老张觉得这样傻等意义不大
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。——来源网络,作者不明。

同步异步、阻塞非阻塞

1.同步阻塞IO

最简单的IO模型,用户线程在读写时被阻塞

数据拷贝指请求到的数据先存放在内核空间, 然后从内核空间拷贝至程序的缓冲区

Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第1张图片
伪代码如下

{
    // read阻塞
    read(socket, buffer);
    // 处理buffer          
    process(buffer);
}

用户线程在IO过程中被阻塞,不能做任何事情,对CPU的资源利用率不高。

2. 同步非阻塞

用户线程不断发起IO请求. 数据未到达时系统返回一状态值; 数据到达后才真正读取数据

Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第2张图片

伪代码如下

{
    // read非阻塞   
    while(read(socket, buffer) != SUCCESS);
    process(buffer);
}

用户线程每次请求IO都可以立即返回,但是为了拿到数据,需不断轮询,无谓地消耗了大量的CPU
一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

3. IO多路复用

IO多路复用建立在内核提供的阻塞函数select上

用户先将需要进行IO操作的socket添加到select中,然后等待阻塞函数select返回。当数据到达后,socket被激活,select返回,用户线程就能接着发起read请求
Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第3张图片
伪代码如下:

复制代码

{
    // 注册
    select(socket);
    // 轮询
    while(true) {
        // 阻塞
        sockets = select();
        // 数据到达, 解除阻塞
        for(socket in sockets) {
            if(can_read(socket)) {
            // 数据已到达, 那么socket阻不阻塞无所谓
       read(socket, buffer);
            process(buffer);
            }
        }
    }
}

看起来和加了循环的同步阻塞IO差不多?

实际上, 我们可以给select注册多个socket, 然后不断调用select读取被激活的socket,实现在同一线程内同时处理多个IO请求的效果.

至此, 同步阻塞(阻塞在select) / 同步非阻塞(IO没有阻塞) {不知道该怎么称呼}完成

4.异步方式

更进一步, 我们把select轮询抽出来放在一个线程里, 用户线程向其注册相关socket或IO请求,等到数据到达时通知用户线程,则可以提高用户线程的CPU利用率.

这样, 便实现了异步方式

Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第4张图片

这其实是 Reactor 设计模式, 如下图, Reactor 详解

Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第5张图片

EventHandler抽象类表示IO事件处理器
get_handle方法获得文件句柄Handle
handle_event方法实现对Handle的操作
可继承EventHandler对事件处理器的行为进行定制

Reactor类管理EventHandler的注册、删除. handle_events方法实现了事件循环, 其不断调用阻塞函数select, 只要某个文件句柄被激活(可读/写等),select就从阻塞中返回, handle_events接着调用与文件句柄关联的事件处理器的handle_event进行相关操作。handler_events的伪代码如下

Reactor::handle_events() {
    while(true) {
        sockets = select();
        for(socket in sockets) {
            get_event_handler(socket).handle_event();
        }
    }
}

作为功能调用者需要实现的伪代码如下

// 继承EventHandler并重写handle_event()方法
void UserEventHandler::handle_event() {
    if(can_read(socket)) {
    // 数据已到达, 那么socket阻不阻塞无所谓
    read(socket, buffer);
    process(buffer);
    }
}
// 注册实现的EventHandler子类
{
    Reactor.register(new UserEventHandler(socket));
}

IO多路复用是最常使用的IO模型,因其轮询select的线程会被阻塞, 异步程度还不够“彻底”, 所以常被称为异步阻塞IO

5.异步IO

真正的异步IO需要操作系统更强的支持。
IO多路复用模型中,数据到达内核后通知用户线程,用户线程负责从内核空间拷贝数据;
而在异步IO模型中,当用户线程收到通知时,数据已经被操作系统从内核拷贝到用户指定的缓冲区内,用户线程直接使用即可。
Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第6张图片
异步IO模型使用了Proactor设计模式实现了这一机制。
Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第7张图片
Reactor模式中,用户线程向Reactor对象注册事件对应的事件处理器,然后事件触发时Reactor调用事件处理函数。
Proactor模式中,用户线程将AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。
AsynchronousOperationProcessor使用Facade模式提供了一组异步API(读/写等)供用户调用. 当用户线程调用异步API后,便继续执行下一步代码. 而此时AsynchronousOperationProcessor会开启独立的内核线程执行异步操作。
当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。
异步IO完成时,AsynchronousOperationProcessor将Proactor和CompletionHandler取出,并将IO操作结果和CompletionHandler分发给Proactor,Proactor通知用户线程(即回调先前注册的事件完成处理类的函数handle_event)。
Proactor一般被实现为单例,以便于集中分发操作完成事件。

伪代码如下

// 继承CompletionHandler, buffer为用户线程指定的缓冲区
void UserCompletionHandler::handle_event(buffer) {
    process(buffer);
}

// 调用异步的read函数
{
    aio_read(socket, new UserCompletionHandler);
}

相比于IO多路复用,异步IO并不常用,因为目前操作系统对异步IO的支持并不完善,IO多路复用也基本够用. 有很多做法是用IO多路复用模型模拟异步IO(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。
JDK7已经支持了AIO, netty采用过又放弃了, 据说是性能提升有限相比多路复用。


非阻塞 同步IO
指的是用户调用读写方法是不阻塞的,立刻返回的,而且需要用户线程来检查IO状态。需要注意的是,如果发现有可以操作的IO,那么实际用户进程还是会阻塞等待内核复制数据到用户进程,它与同步阻塞IO的区别是后者全程等待。
非阻塞 异步IO
的是用户调用读写方法是不阻塞的,立刻返回,而且用户不需要关注读写,只需要提供回调操作,内核线程在完成读写后回调用户提供的callback。

这两个概念的不同造成了编程模型的不同

非阻塞同步IO

非阻塞:无需等待。
同步 IO:自己是操作 IO 的主体,立即返回结果。
由于读写方法非阻塞,并且需要用户自己来进行读写,所以每次调用读写方法实际读写的字节数是不确定的,所以需要一个Buffer来保存每次读写的字节状态。更重要的是用户不知道什么时候完成了读写,一般需要用while循环判断Buffer的状态来跟踪读写。

非阻塞异步IO

非阻塞:无需等待 。
异步:等待 callback
由于是内核线程进行读写,并且在IO完成后会回调用户提供的callback,编程模型就比较简单,用户只需要调用读写,提供回调就可以了,比如 read(filename, callback)

select / poll / epoll 从本质上说都是非阻塞同步IO,select会收到IO就绪的状态,然后通知用户去处理IO,实际的IO操作还需要用户等待内核复制操作。

要理解IO就绪和完成的区别。就绪指的是还需要用户自己去处理,完成指的是内核帮助完成了,用户不用关心IO过程,只需要提供回调函数。

理解了非阻塞同步IO和非阻塞异步IO的区别之后,就不难理解Java NIO的设计了。NIO是围绕ByteBuffer来进行读写的,ByteBuffer是一个缓冲区,用来记录读写的状态,通过多次检查ByteBuffer的状态来确定IO是否完成。

IO为同步阻塞形式,
NIO为同步非阻塞形式、NIO并没有实现异步,
在JDK1.7之后,升级了NIO库包,支持异步阻塞通信模型即NIO2.0(AIO),编程模型大大简化了。用户只需要关注回调函数即可。


缓冲区 Buffer

在NIO 中,所有的数据都是用缓冲区处理的。

缓冲区实质是数组。通常为字节数组 (ByteBuffer)。
Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第8张图片
Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第9张图片

通道 Channel

  • Channel 是一个通道。
    网络数据通过 Channel 读取和写入。
    流只能单向,读写eg: InputStream or OutputStream,
    但是 Channel 可以用于读、写或者二者同时进行。
    Channel 是全双工的,所以更好映射底层系统的 API
    UNIX 网络编程模型中,底层操作系统的通道都是全双工的。同时读写。
    Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第10张图片

多路复用器 Selector

多路复用器提供选择已经就绪的任务的能力。
简单讲:
Selector 会不断轮询注册在其上的 Channel ,如果某个 Channel 上面发生读写时间,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪的 Channel 的集合,进行后续的 I/O 操作。
一个 Selector 可以同时轮序多个 Channel ,
JDK 使用 epoll() 代替传统的 selector 实现,所以没有最大连接句柄 1024/2048 限制。

NIO 服务端序列图

Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第11张图片

NIO 客户端序列图

Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第12张图片

NIO 优点:

  1. 客户端发起的连接操作是异步的。
    可以通过在多路复用器注册 OP_Connect 等待后续结果,不需要要像之前的客户端那样同步阻塞。
  2. SocketChannel 的读写操作都是异步的,
    如果没有可读写的数据它不会同步等待,直接返回。
    这样 I/O 通信线程就可以处理其他的链路,而不需要同步等待这个链路可用。
  3. 线程模型的优化
    JDK Selector 通过 epoll 实现,没有了链接句柄数的限制。

AIO

NIO 2.0 引入了新的异步通道概念,并提供了异步文件通道和异步套接字通道的实现。
在异步 I/O 操作的时候可以传递信号变量,当操作完成之后会回调相关的方法。

I/O 模型对比

按业务需求,设计。
Netty 00 | 原理概念——阻塞非阻塞、异步同步、Reactor_第13张图片

参考内容:

  1. 《Netty 2》 李林峰著作
  2. https://blog.csdn.net/z15732621582/article/details/78939122?foxhandler=RssReadRenderProcessHandler
  3. https://www.cnblogs.com/myJavaEE/p/6721127.html
  4. https://www.zhihu.com/question/19732473?sort=created
  5. https://www.cnblogs.com/doit8791/p/7461479.html

遗留问题:

Proactor 与 Facade模式

你可能感兴趣的:(网络通信)