IO多路复用中的水平触发和边缘触发、Java NIO中的水平触发举例

基础原理

水平触发(Level-triggered,也被称为条件触发)LT:主要满足条件,就触发事件。

边缘触发(Edge-triggered)ET:当状态变化时触发。

使用脉冲信号来说明LT和ET:LT指信号只需要处于水平(高电平、低电平)就会一直触发;ET则是指信号为上升沿或下降沿时才会触发通知。

举例说明:一个管道收到了1kb的数据,epoll会立即返回,此时读了512字节数据,然后再次调用epoll。这时如果是水平触发的,epoll会立即返回,因为有数据准备好了。 如果是边缘触发的不会立即返回,**因为此时虽然有数据可读但是已经触发了一次通知,在这次通知到现在还没有新的数据到来,**直到有新的数据到来epoll才会返回,此时老的数据和新的数据都可以读取到(当然是需要这次你尽可能的多读取)。

在水平触发的情况下,必须不断的轮询监控每个文件描述符的状态,判断其是否可读或可写。内核空间中维护的 I/O 状态列表可能随时会被更新,因此用户程序想要拿到 I/O 状态列表必须访问内核空间。

而边缘触发的情况下,只有在数据到达网卡,也就是说 I/O 状态发生改变时才会触发事件,在两次数据到达的间隙,I/O 状态列表是不会发生改变的。这就使得用户程序可以缓存一份 I/O 状态列表在用户空间中,减少系统调用的次数。

但是在边缘触发的情况下,I/O 操作必须一次性的将数据处理完。因为如果没有处理完数据,只有等待下次数据包到达网卡才会再次触发事件。

在水平触发的情况下,可以处理内核缓冲区中任意长度的数据。如果数据没有处理完,内核会再次触发事件。因此剩余数据在下次事件到来时继续处理即可。

水平触发和边缘触发都需要非阻塞读写。因为它们都属于多路复用技术的实现方式,而使用多路复用技术的触发点便是用更少的线程做更多的事。单线程情况下,无论水平触发还是边缘触发,使用阻塞读写都会造成线程无法处理其它事件的情况。

水平触发和边缘触发的触发时机

水平触发

  • 对于读操作:只要缓冲内容不为空,LT模式返回读就绪。
  • 对于写操作:只要缓冲区内容还不满,ET模式会返回写就绪。

边缘触发

  • 对于读操作:
    • 当缓冲区内容由不可读变为可读的时候,即缓冲区由空变为不为空的时候。
    • 当缓冲区有新数据达到时,即缓冲区中的待读数据变多时。
    • 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。
  • 对于写操作:
    • 当缓冲区由不可写变为可写时。
    • 当有旧数据被发送走时,即缓冲区中的内容变少时。
    • 当缓冲区有空间可写时,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。

Java NIO多路复用技术中的水平触发

JAVA 的 NIO 属于水平触发,而 epoll 既支持水平触发也支持边缘触发。epoll 性能高于 poll 很重要的一点便是 epoll 支持了边缘触发。

代码示例:

Server:

java 复制代码
package netty.netProgram.trigger;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * 验证Java NIO中的水平触发
 */
@Slf4j
public class LevelTrigger {
    public static void main(String[] args) throws IOException {
        server();
    }
    private static void server() throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel severChannel = ServerSocketChannel.open();
        severChannel.configureBlocking(false);
        severChannel.bind(new InetSocketAddress(8888));
        System.out.println("Server start!");
        severChannel.register(selector, SelectionKey.OP_ACCEPT);
        int count = 0;
        //select会阻塞,知道有就绪连接写入selectionKeys
        while (!Thread.currentThread().isInterrupted()) {
            if (selector.select(100) == 0) {
                continue;
            }
            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                //SelectionKey为select中记录的就绪请求的数据结构,其中包括了连接所属的socket及就绪的类型
                SelectionKey key = keys.next();
                //处理事件,不管是否可以处理完成,都删除 key。因为 soketChannel 为水平触发的,
                // 未处理完成的事件删除后会被再次通知
                keys.remove();
                if (key.isAcceptable()) {
                    System.out.println("触发连接事件");
                    SocketChannel socketChannel = severChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(8);
                    int len = socketChannel.read(byteBuffer);
                    byteBuffer.flip();
                    if (len == -1) {
                        key.cancel();
                        socketChannel.close();
                    }
                    count += len;
                    log.debug("current receive data len is : {}, total receive data len is :{}", len, count);
                }
            }
        }
    }
}

Client:

java 复制代码
package netty.netProgram.trigger;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;

@Slf4j
public class LevelTriggerClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 100000; i++) {
                sb.append("a" + i % 26);
            }
            ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
            socketChannel.write(buffer);
            log.debug("client send data len is : {}", sb.toString().length());
            while (true){
                // 死循环,维持客户端和服务端的链接,使得服务端能够完全读取数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

结果演示:

参考链接

I/O多路复用中的水平触发和边缘触发_nio水平触发和边缘触发的区别-CSDN博客

epoll 水平触发与边缘触发_epoll水平触发-CSDN博客

相关推荐
suweijie7682 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿3 小时前
List深拷贝后,数据还是被串改
java
xlsw_6 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹7 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭7 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫7 小时前
泛型(2)
java
超爱吃士力架8 小时前
邀请逻辑
java·linux·后端
南宫生8 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石8 小时前
12/21java基础
java
李小白668 小时前
Spring MVC(上)
java·spring·mvc