posts - 12, comments - 0, trackbacks - 0, articles - 7
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

NIO Selector

Posted on 2011-12-19 18:53 cooperzh 阅读(1274) 评论(0)  编辑  收藏 所属分类: NIO
单一的线程使用就绪选择来同时监控大量的通道

处于就绪状态的通道就会等待Selector选择

选择器提供了询问通道是否已经准备好执行每个IO操作的能力

就绪选择的真正价值在于潜在的大量通道可以同时进行就绪状态的检查,Selector可以轻松决定选择哪个通道

真正的就绪必须由操作系统来检查,操作系统处理IO请求并通知各个线程它们的数据已经已经准备好了,而选择器封装了这种抽象

原因:java代码来进行就绪选择,甚至jvm来进行就绪检查代价都十分昂贵,并且不够原子性,甚至会破坏线程的正常进行

实际上只有三个类用于执行就绪选择:
Selector:管理着一个被注册的通道集合信息和他们的就绪状态
SelectableChannel:被注册到选择器中,并且可以声明对哪种操作感兴趣
一个通道可以注册到多个选择器上
一个通道在一个选择器上只能注册一个操作集,该操作集不会叠加
SelectionKey:封装了特定通道与特定选择器的注册关系,被SelectableChannel.register()返回

SelectableChannel在注册到选择器上之前,先设置为非阻塞模式。
如果注册一个阻塞模式的SelectableChannel将会抛出IllegalBlockingModeException异常
另外,已经注册的SelectableChannel不能再修改阻塞模式,否则也会抛出IllegalBlockingModeException异常
注册一个已经关闭的SelectableChannel,将会抛出ClosedChannelException异常
将SelectableChannel注册到Selector上之后,Selector控制了通道的选择过程

通道有四种操作:
SelectionKey.OP_ACCEPT,
SelectionKey.OP_CONNECT,
SelectionKey.OP_READ,
SelectionKey.OP_WRITE

SocketChannel不支持accept操作

SelectionKey.cancel()方法可以终结通道和选择器的关系,然后SelectionKey会放在Selector.cancelledKeys() 的集合里
取消后SelectionKey会立即失效,但注册不会立即取消,select()调用结束后,cancelledKeys将会处理,相应的注销完成。

关闭通道时,所有的相关的SelectionKey都会自动取消

Selector关闭时,所有注册的通道将会注销,所有相关的SelectionKey会立即失效

一个SelectionKey实例中有两个byte掩码:
instrest集合:指示通道/选择器结合体关心的操作
ready集合:通道已经就绪的操作

instrest集合通过SelectionKey.interestOps()获取,通过SelectionKey.interestOps(int ops)来改变
当相关的Selector正在select()操作时改变SelectionKey的instrest集合,不影响正在进行的select()操作,只会在下次select()时体现

ready集合通过SelectionKey.readyOps()获取,是instrest集合的子集,是上次select()操作后就绪的操作

最简单的状态测试方法是:
SelectionKey.isAcceptable()   等价于 ((key.readyOps( ) & SelectionKey.OP_ACCEPT) != 0
SelectionKey.isConnectable()   等价于 ((key.readyOps( ) & SelectionKey.OP_CONNECT) != 0
SelectionKey.isReadable()   等价于 ((key.readyOps( ) & SelectionKey.OP_READ) != 0
SelectionKey.isWritable()   等价于 ((key.readyOps( ) & SelectionKey.OP_WRITE) != 0

SelectionKey.readyOps()返回的ready集合指示个提示,不是绝对的保证,因为底层的通道在任何时候都可能会被改变

SelectionKey是线程安全的,但修改instrest集合的操作是通过Selector进行同步的
这导致SelectionKey.interestOps()的调用会阻塞不确定长的时间
Selector使用的锁策略是依赖具体实现的

在单线程管理多个通道时不会出现问题,而多个线程共享Selector时会遇到同步的问题,到那时就应该重新设计

每个Selector都要同时维护三个SelectionKey集合:
已注册的键的集合Registered key set,不能直接修改,只能通过SelectionKey.interestOps()重置
已选择的键的集合Selected key set,其中每个SelectionKey都有一个内嵌的ready集合,可以直接移除其中的SelectionKey,但不能添加
已取消的键的集合Cancelled key set,是Selector的私有成员,无法直接访问

Selector的核心是select(),是对操作系统的本地调用(native call)的封装,但是它不仅仅是向操作系统代码传递参数,还对每个select操作应用不同的过程
select()的执行步骤:
1 检查Cancelled key set。如果非空,每个取消的key将从另外两个集合中移除,相关通道被注销,执行完毕,Cancelled key set为空
2 检查Registered key set中每个键的instrest集合,检查中如果对instrest集合有改动,不会影响剩余的检查过程
   底层操作系统将会进行查询,确定每个通道关心的操作的真实就绪状态,依赖特定的select调用。如果没有就绪的通道,则阻塞,直至超时
   这个过程会使调用线程睡眠一段时间,直至通道的就绪状态被确定下来
   对于那些操作系统指示至少已经准备好instrest集合中的一种操作的通道,将执行下面的一种操作:
   a 如果通道的SelectionKey没有处于Selected key set中,那么SelectionKey的ready集合将会清空,表示当前通道已经准备好的操作的Byte掩码将被设置
   b 通道的SelectionKey已经处于Selected key set中,每个SelectionKey的ready集合更新为已经准备好的操作的Byte掩码(SelectionKey.OP_READ ,SelectionKey.OP_WRITE等)
3 步骤2会花费很长时间,特别是所激发的线程处于休眠状态时。步骤2结束时,步骤1会重新执行,以完成任意一个在选择进行的过程当中,键已经被取消的通道的注销
4 select()返回的值是步骤2中被修改的键的数量,即上一个select()调用之后进入就绪状态的通道的数量,而不是Selected key set中通道的数量。之前的调用中已经就绪的,并且在本次调用中仍然就绪的通道不会被计入。返回值有可能为0

使用Cancelled key set来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的好方法

注销通道是一种代价很高的操作,清理已取消的键,并在选择操作之前或之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜在的问题。

Selector的select()有三种形式:
1 Selector.select() 
   该调用在没有通道就绪时将无限阻塞,一旦有至少一个已注册的通道就绪,Selector的选择键将会立即更新,每个选择键的ready集合也会被更新
2 Selector.select(long timeout) 
   在提供的超时时间内没有就绪通道,将会返回0。如果设置timeout=0,则等同于select() 
3 Selector.selectNow() 是完全非阻塞的,会立即返回值

Selector.wakeup() 提供了使线程从被阻塞的select()方法中优雅的退出的能力
同样有三种方式唤醒select()方法中睡眠的线程:
1 Selector.wakeup() 将使Selector上第一个没有返回的select()操作立即返回,如果当前没有正在进行的select()操作,则下次select()操作会立即返回。
2 Selector.close() 被调用时,所有在select()操作中阻塞的线程都将被唤醒,Selector相关通道被注销,相关SelectionKey将被取消
3 Thread.interrupt()被调用时,返回状态将被设置。如果唤醒之后的线程试图在通道上执行IO操作,通道会立即关闭,线程捕捉到一个异常

如果想让一个睡眠线程在中断之后继续进行,需要执行一些步骤来清理中断状态

这些操作不会改变单个相关通道,中断一个Selector和中断一个通道是不一样的。Selector只会检查通道的状态。

当一个select() 操作时睡眠的线程发生了中断,对于通道状态而言,是没有歧义的


选择器把合理的管理SelectionKey,以确保它们表示的状态不会变得陈旧的任务交给了程序员

当SelectionKey已经不在选择器的Selected key set中时,会发生什么

当通道上至少一个感兴趣的操作就绪时,SelectionKey的ready集合会被清空,并且当前已经就绪的操作会被添加到ready集合里,该SelectionKey随后会被添加到Selected key set中

清理一个SelectionKey的ready集合的方式是将这个key从Selected key set中移除

SelectionKey的就绪状态只有在Selector的select() 操作过程中才会修改,因为只有Selected key set中的SelectionKey才被认为是包含了合法的就绪信息的,这些信息在SelectionKey中长久存在,知道key从Selected key set中移除,以通知Selector你已经看到并对它进行了处理。当下一次通道的感兴趣的操作发生时,key将被重新设置以反映当时通道的状态,并再次被添加到Selected key set中

这种框架提供了非常大的灵活性。

常规做法是先调用select() 操作,再遍历selectKeys()返回的key的集合。在按顺序进行检查每个key的过程中,相关的通道也根据key的就绪集合进行处理。然后key从Selected key set中移除,检查下一个key。完成后通过调用select() 重复循环。
服务器端使用方法
1 创建ServerSocketChannel,注册到Selector中,感兴趣的操作为accept
2 轮询Selector的select()操作,从就绪key集合中遍历key的ready集合,有accept则调用ServerSocketChannel的accept()方法获取SocketChannel,其中包含接收到的socket的句柄。再将SocketChannel注册到Selector中感兴趣的操作为read
3 当下次select()操作中key的ready集合中有read时,开始做事

Selector是线程安全的,但key set不是。Selector.keys 和 Selector.selectkeys() 返回的是Selector内部私有key set的引用。这个集合可能随时被改变。迭代器Iterator是快速失败的(fail-fast),如果迭代的时候key set发生改变,抛出ConcurrentModificationException。所有如果希望在多个线程间共享Selector或SelectionKey,则要对此做好准备。当你修改一个key的时候,可能会破坏另一个线程的Iterator

如果在多个线程并发的访问一个Selector的key set时,需要合理地同步访问。在select()操作时,先在Selector上进行同步,再是Registered key set,最后是Selected key set。按照这样的顺序,Cancelled key set就会在第一步和第三步之间保持同步。

在多线程的场景中,如果需要对任何一个key set进行更改,不管是直接更改还是其他操作带来的副作用,都需要以相同的顺序,在同一对象上进行同步。如果竞争的线程没有以同样的顺序请求锁,则会有死锁的隐患。

Selector的close()和select()操作都会有一直阻塞的可能,当在select()的过程当中,所有对close()的调用都会被阻塞,直到select()结束,或者执行select()的线程进入睡眠。当select()的线程进入睡眠时,close()的线程获得锁后会立即唤醒select()的线程,并关闭Selector
如果不采取相同的顺序同步,则key set中key的信息不保证是有效的,相关通道也不保证是打开的


一个cpu的时候使用一个线程来管理所有通道,是一个合适的解决方案,但会浪费其他cpu的运行能力

在大量通道上执行select()操作不会有太大开销,因为大多数工作都是操纵系统完成的

方案1 管理多个Selector,并随机分配通道不是一个好方案
方案2 所有通道使用一个Selector,将就绪通道的服务委托给其他线程。相当于用一个线程监控通道的就绪状态,使用另外的工作线程池来处理接收到的数据。而线程池是可以调整的,或者动态调整。

在方案2中,如果某些通道要求更高的响应速度,可以用两个选择器来解决。并且线程池可以细化为日志线程池、命令线程池、状态请求线程池等