前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「高并发通信框架Netty4 源码解读(七)」NIO通道之Selector选择器

「高并发通信框架Netty4 源码解读(七)」NIO通道之Selector选择器

作者头像
源码之路
发布2020-09-04 10:21:44
4710
发布2020-09-04 10:21:44
举报
文章被收录于专栏:源码之路源码之路

选择器提供选择执行已经就绪的任务的能力,这使得多元 I/O 成为可能。就绪选择和多元执行使得单线程能够有效率地同 时管理多个 I/O 通道(channels)。 C/C++代码的工具箱中,许多年前就已经有 select()和 poll()这两个POSIX(可移植性操作系统接口)系统调用可供使用了。许过操作系统也提供相似的功能,但对Java 程序员来说,就绪选择功能直到 JDK 1.4 才成为可行的方案。对于主要的工作经验都是基于Java 环境的开发的程序员来说,之前可能还没有碰到过这种 I/O 模型。

选择器是什么

想象一下,一个有三个传送通道的银行。在传统的(非选择器)的场景里,想象一下每个银行的传送通道都有一个气动导管,传送到银行里它对应的出纳员的窗口,并且每一个窗口与其他窗口是用墙壁分隔开的。这意味着每个导管(通道)需要一个专门的出纳员(工作线程)。这种方式不易于扩展,而且也是十分浪费的。对于每个新增加的导管(通道),都需要一个新的出纳员,以及其他相关的经费,如表格、椅子、纸张的夹子(内存、 CPU 周期、上下文切换)等等。并且当事情变慢下来时,这些资源(以及相关的花费)大多数时候是闲置的。

现在想象一下另一个不同的场景,每一个气动导管(通道)都只与一个出纳员的窗口连接。这个窗口有三个槽可以放置运输过来的物品(数据缓冲区),每个槽都有一个指示器(选择键,selection key),当运输的物品进入时会亮起。同时想象一下出纳员(工作线程)有一个花尽量多的时间阅读《自己动手编写个人档案》一书的癖好。在每一段的最后,出纳员看一眼指示灯(调用select( )函数),来决定人一个通道是否已经就绪(就绪选择)。在传送带闲置时,出纳员(工作线程)可以做其他事情,但需要注意的时候又可以进行及时的处理。

虽然这种分析并不精确,但它描述了快速检查大量资源中的任意一个是否需要关注,而在某些东西没有准备好时又不必被迫等待的通用模式。这种检查并继续的能力是可扩展性的关键,它使得仅仅使用单一的线程就可以通过就绪选择来监控大量的通道。选择器及相关的类就提供了这种 API,使得我们可以在通道上进行就绪选择。

选择器基础

您需要将前面博客创建的一个或多个可选择的通道注册到选择器对象中。一个表示通道和选择器的键将会被返回。选择键会记住您关心的通道。它们也会追踪对应的通道是否已经就绪。当您调用一个选择器对象的 select( )方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道。您可以获取一个键的集合,从而找到当时已经就绪的通道。通过遍历这些键,您可以选择出每个从上次您调用 select( )开始直到现在,已经就绪的通道。

从最基础的层面来看,选择器提供了询问通道是否已经准备好执行每个 I/0 操作的能力。例如,我们需要了解一个 SocketChannel 对象是否还有更多的字节需要读取,或者我们需要知道ServerSocketChannel 是否有需要准备接受的连接。

在与 SelectableChannel 联合使用时,选择器提供了这种服务,但这里面有更多的事情需要去了解。就绪选择的真正价值在于潜在的大量的通道可以同时进行就绪状态的检查。调用者可以轻松地决定多个通道中的哪一个准备好要运行。有两种方式可以选择:被激发的线程可以处于休眠状态,直到一个或者多个注册到选择器的通道就绪,或者它也可以周期性地轮询选择器,看看从上次检查之后,是否有通道处于就绪状态。

如果您考虑一下需要管理大量并发的连接的网络服务器(webserver)的实现,就可以很容易地想到如何善加利用这些能力。 乍一看,好像只要非阻塞模式就可以模拟就绪检查功能,但实际上还不够。非阻塞模式同时还会执行您请求的任务,或指出它无法执行这项任务。这与检查它是否能够执行某种类型的操作是不同的。举个例子,如果您试图执行非阻塞操作,并且也执行成功了,您将不仅仅发现 read( )是可以执行的,同时您也已经读入了一些数据。接下来您就需要处理这些数据了。效率上的要求使得您不能将检查就绪的代码和处理数据的代码分离开来,至少这么做会很复杂。

即使简单地询问每个通道是否已经就绪的方法是可行的,在您的代码或一个类库的包里的某些代码需要遍历每一个候选的通道并按顺序进行检查的时候,仍然是有问题的。这会使得在检查每个通道是否就绪时都至少进行一次系统调用,这种代价是十分昂贵的,但是主要的问题是,这种检查不是原子性的。列表中的一个通道都有可能在它被检查之后就绪,但直到下一次轮询为止,您并不会觉察到这种情况。最糟糕的是,您除了不断地遍历列表之外将别无选择。您无法在某个您感兴趣的通道就绪时得到通知。

这就是为什么传统的监控多个 socket 的 Java 解决方案是为每个 socket 创建一个线程并使得线程可以在 read( )调用中阻塞,直到数据可用。这事实上将每个被阻塞的线程当作了 socket 监控器,并将 Java 虚拟机的线程调度当作了通知机制。这两者本来都不是为了这种目的而设计的。程序员和 Java 虚拟机都为管理所有这些线程的复杂性和性能损耗付出了代价,这在线程数量的增长失控时表现得更为突出。

真正的就绪选择必须由操作系统来做。操作系统的一项最重要的功能就是处理 I/O 请求并通知各个线程它们的数据已经准备好了。选择器类提供了这种抽象,使得 Java 代码能够以可移植的方式,请求底层的操作系统提供就绪选择服务。让我们看一下 java.nio.channels 包中处理就绪选择的特定的类。

选择器,可选择通道和选择键类

现在,您也许还对这些用于就绪选择的 Java 成员感到困惑。让我们来区分这些活动的零件并了解它们是如何交互的吧。UML 图使得情形看起来比真实的情况更为复杂了:

实际上只有三个有关的类 API,用于执行就绪选择:

  • 选择器(Selector) 选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。
  • 可选择通道(SelectableChannel) 这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。 FileChannel 对象不是可选择的,因为它们没有继承 SelectableChannel。所有 socket 通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。 SelectableChannel 可以被注册到 Selector 对象上,同时可以指定对哪个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
  • 选择键(SelectionKey) 选择键封装了特定的通道与特定的选择器的注册关系 。 选择键对象被SelectableChannel.register( ) 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

让我们看看 SelectableChannel 的相关 API 方法

代码语言:javascript
复制
public abstract class SelectableChannel extends AbstractChannel implements Channel
{
  // This is a partial API listing
  public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException;
  public abstract SelectionKey register (Selector sel, int ops,Object att) throws ClosedChannelException;
  public abstract boolean isRegistered( );
  public abstract SelectionKey keyFor (Selector sel);
  public abstract int validOps( );
  public abstract void configureBlocking (boolean block) throws IOException;
  public abstract boolean isBlocking( );
  public abstract Object blockingLock( );
}

非阻塞特性与多元执行特性的关系是十分密切的——以至于 java.nio 的架构将两者的 API放到了一个类中。我们已经探讨了如何用上面列出的 SelecableChannel 的最后三个方法来配置并检查通道的阻塞模式(上一篇讲过)。通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用 configureBlocking(false))。

调用可选择通道的 register( )方法会将它注册到一个选择器上。如果您试图注册一个处于阻塞状态的通道, register( )将抛出未检查的 IllegalBlockingModeException 异常。此外,通道一旦被注册,就不能回到阻塞状态。试图这么做的话,将在调用 configureBlocking( )方法时将抛出IllegalBlockingModeException 异常。

并且,理所当然地,试图注册一个已经关闭的 SelectableChannel 实例的话,也将抛出ClosedChannelException 异常,就像方法原型指示的那样。

在我们进一步了解 register( )和 SelectableChannel 的其他方法之前,让我们先了解一下Selector 类的 API,以确保我们可以更好地理解这种关系:

代码语言:javascript
复制
public abstract class Selector
{
  public static Selector open( ) throws IOException
  public abstract boolean isOpen( );
  public abstract void close( ) throws IOException;
  public abstract SelectionProvider provider( );
  public abstract int select( ) throws IOException;
  public abstract int select (long timeout) throws IOException;
  public abstract int selectNow( ) throws IOException;
  public abstract void wakeup( );
  public abstract Set keys( );
  public abstract Set selectedKeys( );
}

尽管 SelectableChannel 类上定义了 register( )方法,还是应该将通道注册到选择器上,而不是另一种方式。选择器维护了一个需要监控的通道的集合。一个给定的通道可以被注册到多于一个的选择器上 , 而且不需要知道它被注册了那Selector 对 象 上 。 将 register( )放在SelectableChannel 上而不是 Selector 上,这种做法看起来有点随意。它将返回一个封装了 两个对象的关系的选择键对象。重要的是要记住选择器对象控制了被注册到它之上的通道的选择过程。

代码语言:javascript
复制
public abstract class SelectionKey
{
  public static final int OP_READ
  public static final int OP_WRITE
  public static final int OP_CONNECT
  public static final int OP_ACCEPT
  public abstract SelectableChannel channel( );
  public abstract Selector selector( );
  public abstract void cancel( );
  public abstract boolean isValid( );
  public abstract int interestOps( );
  public abstract void interestOps (int ops);
  public abstract int readyOps( );
  public final boolean isReadable( )
  public final boolean isWritable( )
  public final boolean isConnectable( )
  public final boolean isAcceptable( )
  public final Object attach (Object ob)
  public final Object attachment( )
}

择器才是提供管理功能的对象,而不是可选择通道对象。选择器对象对注册到它之上的通道执行就绪选择,并管理选择键。 对于键的 interest(感兴趣的操作)集合和 ready(已经准备好的操作)集合的解释是和特定的通道相关的。每个通道的实现,将定义它自己的选择键类。在 register( )方法中构造它并将它传递给所提供的选择器对象。

建立选择器

现在您可能仍然感到困惑,您在前面的三个清单中看到了大量的方法,但无法分辨出它们具体做什么,或者它们代表了什么意思。在钻研所有这一切的细节之前,让我们看看一个经典的应用实例。它可以帮助我们将所有东西放到一个特定的上下文中去理解。

代码语言:javascript
复制
Selector selector = Selector.open( );
channel1.register (selector, SelectionKey.OP_READ);
channel2.register (selector, SelectionKey.OP_WRITE);
channel3.register (selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// Wait up to 10 seconds for a channel to become ready
readyCount = selector.select (10000);

这些代码创建了一个新的选择器,然后将这三个(已经存在的)socket 通道注册到选择器上,而且感兴趣的操作各不相同。select( )方法在将线程置于睡眠状态,直到这些刚兴趣的事情中的操作中的一个发生或者 10 秒钟的时间过去。 现在让我们看看 Selector 的 API 的细节:

代码语言:javascript
复制
public abstract class Selector
{
  // This is a partial API listing
  public static Selector open( ) throws IOException
  public abstract boolean isOpen( );
  public abstract void close( ) throws IOException;
  public abstract SelectionProvider provider( );
}

Selector 对象是通过调用静态工厂方法 open( )来实例化的。选择器不是像通道或流(stream)那样的基本 I/O 对象:数据从来没有通过它们进行传递。类方法 open( )向 SPI 发出请求,通过默认的 SelectorProvider 对象获取一个新的实例。

通过调用一个自定义的 SelectorProvider对象的 openSelector( )方法来创建一个 Selector 实例也是可行的。您可以通过调用 provider( )方法来决定由哪个 SelectorProvider 对象来创建给定的 Selector 实例。大多数情况下,您不需要关心 SPI;只需要调用 open( )方法来创建新的 Selector 对象。

当您不再使用它时,需要调用 close( )方法来释放它可能占用的资源并将所有相关的选择键设置为无效。一旦一个选择器被关闭,试图调用它的大多数方法都将导致 ClosedSelectorException。注意 ClosedSelectorException是一个非检查(运行时的)错误。您可以通过 isOpen( )方法来测试一个选择器是否处于被打开的状态。

让我们看看如何将通道注册到选择器上。下面是一个之前讲过的 SelectableChannel 的 API 的简化版本

代码语言:javascript
复制
public abstract class SelectableChannel extends AbstractChannel
implements Channel
{
  // This is a partial API listing
public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException;
public abstract SelectionKey register (Selector sel, int ops,Object att) throws ClosedChannelException;
public abstract boolean isRegistered( );
public abstract SelectionKey keyFor (Selector sel);
public abstract int validOps( );
}

就像之前提到的那样, register( )方法位于 SelectableChannel 类,尽管通道实际上是被注册到选择器上的。您可以看到 register( )方法接受一个 Selector 对象作为参数,以及一个名为ops 的整数参数。第二个参数表示所关心的通道操作。这是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。特定的操作比特值在 SelectonKey 类中被定义为 public static 字段。

在 JDK 1.4 中,有四种被定义的可选择操作:读(read),写(write),连接(connect)和接受(accept)。

并非所有的操作都在所有的可选择通道上被支持。例如, SocketChannel 不支持 accept。试图注册不支持的操作将导致 IllegalArgumentException。您可以通过调用 validOps( )方法来获取特定的通道所支持的操作集合。

选择器包含了注册到它们之上的通道的集合。在任意给定的时间里,对于一个给定的选择器和一个给定的通道而言,只有一种注册关系是有效的。但是,将一个通道注册到多于一个的选择器上允许的。这么做的话,在更新 interest 集合为指定的值的同时,将返回与之前相同的选择键。实际上,后续的注册都只是简单地将与之前的注册关系相关的键进行更新。

一个例外的情形是当您试图将一个通道注册到一个相关的键已经被取消的选择器上,而通道仍然处于被注册的状态的时候。通道不会在键被取消的时候立即注销。直到下一次操作发生为止,它们仍然会处于被注册的状态。在这种情况下,未检查的 CancelledKeyException将会被抛出。请务必在键可能被取消的情况下检查 SelectionKey 对象的状态。

在之前的清单中,您可能已经注意到了 register( )的第二个版本,这个版本接受 object 参数。这是一个方便的方法,可以传递您提供的对象引用,在调用新生成的选择键的 attach( )方法时会将这个对象引用返回给您。我们将会在下一节更进一步地了解 SelectionKey 的 API。

一个单独的通道对象可以被注册到多个选择器上。可以调用 isRegistered( )方法来检查一个通道是否被注册到任何一个选择器上。这个方法没有提供关于通道被注册到哪个选择器上的信息,而只能知道它至少被注册到了一个选择器上。 此外,在一个键被取消之后,直到通道被注销为止,可能有时间上的延迟。这个方法只是一个提示,而不是确切的答案。

任何一个通道和选择器的注册关系都被封装在一个 SelectionKey 对象中。 keyFor( )方法将返回与该通道和指定的选择器相关的键。如果通道被注册到指定的选择器上,那么相关的键将被返回。如果它们之间没有注册关系,那么将返回 null。

使用选择键

让我们看看 SelectionKey 类的 API:

代码语言:javascript
复制
package java.nio.channels;
public abstract class SelectionKey
{
  public static final int OP_READ
  public static final int OP_WRITE
  public static final int OP_CONNECT
  public static final int OP_ACCEPT
  public abstract SelectableChannel channel( );
  public abstract Selector selector( );
  public abstract void cancel( );
  public abstract boolean isValid( );
  public abstract int interestOps( );
  public abstract void interestOps (int ops);
  public abstract int readyOps( );
  public final boolean isReadable( )
  public final boolean isWritable( )
  public final boolean isConnectable( )
  public final boolean isAcceptable( )
  public final Object attach (Object ob)
  public final Object attachment( )
}

就像之前提到的那样,一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系 。 您可以看到前两个方法中反映了这种关系 。 channel( ) 方法返回与该键相关的SelectableChannel 对象,而 selector( )则返回相关的 Selector 对象。这没有什么令人惊奇的。 键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey对象的 cancel( )方法。可以通过调用 isValid( )方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。

当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey 将被返回。

当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出 CancelledKeyException。

一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest 集合),另一个表示通道准备好要执行的操作(ready 集合)。

当前的 interest 集合可以通过调用键对象的 interestOps( )方法来获取。最初,这应该是通道被注册时传进来的值。这个 interset 集合永远不会被选择器改变,但您可以通过调用 interestOps( )方法并传入一个新的比特掩码参数来改变它。 interest 集合也可以通过将通道注册到选择器上来改变(实际上使用一种迂回的方式调用 interestOps( ))。

当相关的 Selector 上的 select( )操作正在进行时改变键的 interest 集合,不会影响那个正在进行的选择操作。所有更改将会在 select( )的下一个调用中体现出来。

可以通过调用键的 readyOps( )方法来获取相关的通道的已经就绪的操作。 ready 集合是 interest集合的子集,并且表示了 interest 集合中从上次调用 select( )以来已经就绪的那些操作。

例如,下面的代码测试了与键关联的通道是否就绪。如果就绪,就将数据读取出来,写入一个缓冲区,并将它送到一个 consumer(消费者)方法中。

代码语言:javascript
复制
if ((key.readyOps( ) & SelectionKey.OP_READ) != 0)
{
  myBuffer.clear( );
  key.channel( ).read (myBuffer);
  doSomethingWithBuffer (myBuffer.flip( ));
}

就像之前提到过的那样,有四个通道操作可以被用于测试就绪状态。您可以像上面的代码那样,通过测试比特掩码来检查这些状态,但 SelectionKey 类定义了四个便于使用的布尔方法来为您测试这些比特值: isReadable( ), isWritable( ), isConnectable( ), 和 isAcceptable( )。每一个方法都与使用特定掩码来测试 readyOps( )方法的结果的效果相同。例如: if (key.isWritable( ))等价于:if ((key.readyOps( ) & SelectionKey.OP_WRITE) != 0)

这四个方法在任意一个 SelectionKey 对象上都能安全地调用。不能在一个通道上注册一个它不支持的操作,这种操作也永远不会出现在 ready 集合中。调用一个不支持的操作将总是返回 false,因为这种操作在该通道上永远不会准备好。

需要注意的是,通过相关的选择键的 readyOps( )方法返回的就绪状态指示只是一个提示,不是保证。底层的通道在任何时候都会不断改变。其他线程可能在通道上执行操作并影响它的就绪状态。同时,操作系统的特点也总是需要考虑的。

您可能会从 SelectionKey 的 API 中注意到尽管有获取 ready 集合的方法,但没有重新设置那个集合的成员方法。事实上,您不能直接改变键的 ready 集合。

让我们试验一下 SelectionKey 的 API 中剩下的两个方法:

代码语言:javascript
复制
public abstract class SelectionKey
{
  / / This is a partial API listing
  public final Object attach (Object ob)
  public final Object attachment( )
}

这两个方法允许您在键上放置一个“附件”,并在后面获取它。这是一种允许您将任意对象与键关联的便捷的方法。这个对象可以引用任何对您而言有意义的对象,例如业务对象、会话句柄、其他通道等等。这将允许您遍历与选择器相关的键,使用附加在上面的对象句柄作为引用来获取相关的上下文。

attach( )方法将在键对象中保存所提供的对象的引用。 SelectionKey 类除了保存它之外,不会将它用于任何其他用途。任何一个之前保存在键中的附件引用都会被替换。可以使用 null 值来清除附件。可以通过调用 attachment( )方法来获取与键关联的附件句柄。如果没有附件,或者显式地通过 null 方法进行过设置,这个方法将返回 null。

SelectableChannel 类的一个 register( )方法的重载版本接受一个 Object 类型的参数。这是一个方便您在注册时附加一个对象到新生成的键上的方法。以下代码: SelectionKey key = channel.register (selector, SelectionKey.OP_READ, myObject); 等价于: SelectionKey key = channel.register (selector, SelectionKey.OP_READ); key.attach (myObject);

关于 SelectionKey 的最后一件需要注意的事情是并发性。总体上说, SelectionKey 对象是线程安全的,但知道修改 interest 集合的操作是通过 Selector 对象进行同步的是很重要的。这可能会导致 interestOps( )方法的调用会阻塞不确定长的一段时间。选择器所使用的锁策略(例如是否在整个选择过程中保持这些锁)是依赖于具体实现的。幸好,这种多元处理能力被特别地设计为可以使用单线程来管理多个通道。被多个线程使用的选择器也只会在系统特别复杂时产生问题。坦白地说,如果您在多线程中共享选择器时遇到了同步的问题,也许您需要重新思考一下您的设计。

我们已经探讨了 SelectionKey 的 API,但我们还没有谈完选择键的一切——远远没有。让我们进一步了解如何使用选择器管理键吧。

使用选择器

在详细了解 API 之前,您需要知道一点和 Selector 内部工作原理相关的知识。就像上面探讨的那样,选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey 对象中的。每一个 Selector 对象维护三个键的集合:

代码语言:javascript
复制
public abstract class Selector
{
// This is a partial API listing
public abstract Set keys( );
public abstract Set selectedKeys( );
public abstract int select( ) throws IOException;
public abstract int select (long timeout) throws IOException;
public abstract int selectNow( ) throws IOException;
public abstract void wakeup( );
}
  • 已注册的键的集合(Registered key set) 与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys( )方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引 java.lang.UnsupportedOperationException。
  • 已选择的键的集合(Selected key set) 已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的 interest 集合中的操作。这个集合通过 selectedKeys( )方法返回(并有可能是空的)。不要将已选择的键的集合与 ready 集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的 ready 集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。
  • 已取消的键的集合(Cancelled key set) 已注册的键的集合的子集,这个集合包含了 cancel( )方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。在一个刚初始化的 Selector 对象中,这三个集合都是空的。Selector 类的核心是选择过程。这个名词您已经在之前看过多次了——现在应该解释一下了。基本上来说,选择器是对 select( )、 poll( )等本地调用(native call)或者类似的操作系统特定的系统调用的一个包装。但是 Selector 所作的不仅仅是简单地向本地代码传送参数。它对每个选择操作应用了特定的过程。对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。

选择操作是当三种形式的 select( )中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:

  1. 已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
  2. 已注册的键的集合中的键的 interest 集合将被检查。在这个步骤中的检查执行过后,对interest 集合的改动不会影响剩余的检查过程。一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态。依赖于特定的 select( )方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。直到系统调用完成为止,这个过程可能会使得调用线程睡眠一段时间,然后当前每个通道的就绪状态将确定下来。对于那些还没准备好的通道将不会执行任何的操作。对于那些操作系统指示至少已经准备好 interest 集合中的一种操作的通道,将执行以下两种操作中的一种: a.如果通道的键还没有处于已选择的键的集合中,那么键的 ready 集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。 b.否则,也就是键在已选择的键的集合中。键的 ready 集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的 ready 集合是与之前的 ready 集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的 ready 集合将是累积的。比特位只会被设置,不会被清理。
  3. 步骤 2 可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会同时被取消。当步骤 2 结束时,步骤 1 将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销。
  4. select 操作返回的值是 ready 集合在步骤 2 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。

使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化。注销通道是一个潜在的代价很高的操作,这可能需要重新分配资源(请记住,键是与通道相关的,并且可能与它们相关的通道对象之间有复杂的交互)。清理已取消的键,并在选择操作之前和之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜在棘手问题。这是另一个兼顾健壮性的折中方案。

Selector 类的 select( )方法有以下三种不同的形式: 这三种 select 的形式,仅仅在它们在所注册的通道当前都没有就绪时,是否阻塞的方面有所不同。最简单的没有参数的形式可以用如下方式调用:

这种调用在没有通道就绪时将无限阻塞。一旦至少有一个已注册的通道就绪,选择器的选择键就会被更新,并且每个就绪的通道的 ready 集合也将被更新。返回值将会是已经确定就绪的通道的数目。正常情况下, 这些方法将返回一个非零的值,因为直到一个通道就绪前它都会阻塞。但是它也可以返回非 0 值,如果选择器的 wakeup( )方法被其他线程调用。

有时您会想要限制线程等待通道就绪的时间。这种情况下,可以使用一个接受一个超时参数的select( )方法的重载形式:

这种调用与之前的例子完全相同,除了如果在您提供的超时时间(以毫秒计算)内没有通道就绪时,它将返回 0。如果一个或者多个通道在时间限制终止前就绪,键的状态将会被更新,并且方法会在那时立即返回。将超时参数指定为 0 表示将无限期等待,那么它就在各个方面都等同于使用无参数版本的 select( )了。

就绪选择的第三种也是最后一种形式是完全非阻塞的: int n = selector.selectNow( ); selectNow()方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。

停止选择过程

Selector 的 API 中的最后一个方法, wakeup( ),提供了使线程从被阻塞的 select( )方法中优雅地退出的能力:

代码语言:javascript
复制
public abstract class Selector
{
// This is a partial API listing
public abstract void wakeup( );
}

有三种方式可以唤醒在 select( )方法中睡眠的线程:

  • 调用 wakeup( ) 调用 Selector 对象的 wakeup( )方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对 select( )方法的一种形式的调用将立即返回。后续的选择操作将正常进行。在选择操作之间多次调用 wakeup( )方法与调用它一次没有什么不同。

有时这种延迟的唤醒行为并不是您想要的。您可能只想唤醒一个睡眠中的线程,而使得后续的选择继续正常地进行。您可以通过在调用 wakeup( )方法后调用 selectNow( )方法来绕过这个问题。尽管如此,如果您将您的代码构造为合理地关注于返回值和执行选择集合,那么即使下一个 select( )方法的调用在没有通道就绪时就立即返回,也应该不会有什么不同。不管怎么说,您应该为可能发生的事件做好准备。

  • 调用 close( ) 如果选择器的 close( )方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像wakeup( )方法被调用了一样。与选择器相关的通道将被注销, 而键将被取消。
  • 调用 interrupt( ) 如果睡眠中的线程的 interrupt( )方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。使用 wakeup( )方法将会优雅地将一个在 select( )方法中睡眠的线程唤醒。如果您想让一个睡眠的线程在直接中断之后继续执行,需要执行一些步骤来清理中断状态。

Selector 对象将捕捉 InterruptedException 异常并调用 wakeup( )方法。请注意这些方法中的任意一个都不会关闭任何一个相关的通道。中断一个选择器与中断一个通道是不一样的。选择器不会改变任意一个相关的通道,它只会检查它们的状态。当一个在 select( )方法中睡眠的线程中断时,对于通道的状态而言,是不会产生歧义的。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 选择器是什么
  • 选择器基础
  • 选择器,可选择通道和选择键类
  • 建立选择器
  • 使用选择键
  • 使用选择器
  • 停止选择过程
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档