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博客

相关推荐
七星静香24 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员25 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU26 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie629 分钟前
在IDEA中使用Git
java·git
Elaine20239144 分钟前
06 网络编程基础
java·网络
G丶AEOM1 小时前
分布式——BASE理论
java·分布式·八股
落落鱼20131 小时前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存
镰刀出海1 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel