• 沒有找到結果。

使用选择器

在文檔中 Ron Hitchens 著 裴小星 译 (頁 139-0)

第四章 选择器

4.3 使用选择器

既然我们已经很好地掌握了了各种不同类以及它们之间的关联,那么现在让我们进一步了解 Selector 类,也就是就绪选择的核心。这里是 Selector 类的可用的 API。在 4.1.2 小节中,我们已 经看到如何创建新的选择器,那么那些方法还剩下:

4.3.1 选择过程

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

public abstract class Selector {

// This is a partial API listing public abstract Set keys( );

public abstract Set selectedKeys( );

如果选择键的存续时间很长,但您附加的对象不应该存在那么长时 间,请记得在完成后清理附件。否则,您附加的对象将不能被垃圾回 收,您将会面临内存泄漏问题。

136 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.已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两 个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。

137 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 的形式,仅仅在它们在所注册的通道当前都没有就绪时,是否阻塞的方面有所不 同。最简单的没有参数的形式可以用如下方式调用:

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

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

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

就绪选择的第三种也是最后一种形式是完全非阻塞的:

int n = selector.selectNow( );

selectNow()方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。

4.3.2 停止选择过程

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

public abstract class Selector {

// This is a partial API listing public abstract void wakeup( );

}

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

调用 wakeup( )

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

有时这种延迟的唤醒行为并不是您想要的。您可能只想唤醒一个睡眠中的线程,而使得后续的 选择继续正常地进行。您可以通过在调用 wakeup( )方法后调用 selectNow( )方法来绕过这个问题。

尽管如此,如果您将您的代码构造为合理地关注于返回值和执行选择集合,那么即使下一个 select( ) 方法的调用在没有通道就绪时就立即返回,也应该不会有什么不同。不管怎么说,您应该为可能发 生的事件做好准备。

139

调用 close( )

如果选择器的 close( )方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像 wakeup( )方法被调用了一样。与选择器相关的通道将被注销,而键将被取消。

调用 interrupt( )

如果睡眠中的线程的 interrupt( )方法被调用,它的返回状态将被设置。如果被唤醒的线程之后 将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。这是由于在第三章 中已经探讨过的通道的中断语义。使用 wakeup( )法将会优雅地将一个在 select( )方法中睡眠的 线程唤醒。如果您想让一个睡眠的线程在直接中断之后继续执行,需要执行一些步骤来清理中断状 态(参见 Thread.interrupted( )的相关文档)。

Selector 对象将捕捉 InterruptedException 异常并调用wakeup( )方法。

请注意这些方法中的任意一个都不会关闭任何一个相关的通道。中断一个选择器与中断一个通 道是不一样的(参见 3.3 节)。选择器不会改变任意一个相关的通道,它只会检查它们的状态。当 一个在 select( )方法中睡眠的线程中断时,对于通道的状态而言,是不会产生歧义的。

4.3.3 管理选择键

既然我们已经理解了问题的各个部分是怎样结合在一起的,那么是时候看看它们在正常的使用 中是如何交互的了。为了有效地利用选择器和键提供的信息,合理地管理键是非常重要的。

选择是累积的。一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个

选择是累积的。一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个

在文檔中 Ron Hitchens 著 裴小星 译 (頁 139-0)

相關文件