2017-10-07 108 views
5

考虑请求 - 响应协议。如何在请求处理线程和SocketChannel选择器线程之间建立一个前后关系?

我们产生了一个线程来执行select()循环,用于在已接受的非阻塞SocketChannel上进行读取和写入操作。这可能看起来像

while (!isStopped()) { 
    selector.select(); 
    Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); 

    while (selectedKeys.hasNext()) { 
     SelectionKey selectedKey = selectedKeys.next(); 
     selectedKeys.remove(); 
     Context context = (Context) selectedKey.attachment(); 
     if (selectedKey.isReadable()) { 
      context.readRequest(); 
     } else /* if (selectedKey.isWritable()) */ { 
      context.writeResponse(); 
     } 
    } 
} 

其中Context仅仅是相应SocketChannel容器,缓冲器和逻辑读入并从中写。该readRequest可能看起来像

public void readRequest() { 
    // read all content 
    socketChannel.read(requestBuffer); 
    // not interested anymore 
    selectionKey.interestOps(0); 
    executorService.submit(() -> { 
     // handle request with request buffer and prepare response 
     responseBuffer.put(/* some response content */); // or set fields of some bean that will be serialized 

     // notify selector, ready to write 
     selectionKey.interestOps(SelectionKey.OP_WRITE); 
     selectionKey.selector().wakeup(); // worried about this 
    }); 
} 

换句话说,我们从套接字通道读取,填充一些缓冲和挂断处理一些其他线程。该线程完成处理并准备将其存储在响应缓冲区中的响应。然后通知选择器它想要写入并唤醒它。

Javadoc Selector#wakeup()没有提及任何发生之前的关系,所以我担心选择器线程可能会看到响应缓冲区(或某个中间对象)处于不一致状态。

这是一种可能的情况?如果是这样的话,有什么正确的方法可以通过Selector循环线程将响应写入SocketChannel? (通过一些volatile字段发布响应?使用SelectionKey附件?一些其他形式的同步?)

+0

这似乎是[这个问题](https://stackoverflow.com/q/28754275/149138),它也没有很好的答案。我的回答可能是,在实际中,在一个线程上的wakeup()和另一个接收唤醒的线程上的select()调用之间发生了一个事件之前的关系。这有两个实际原因:因为它以任何其他方式工作都会使API非常无用,典型实现的内部机制将涉及相同类型的原子和锁定原语,这些原语会在订购前执行。我认为这是一个文档缺陷。 – BeeOnRope

回答

2

documentation on Selector说以下内容:

选择操作同步的选择本身,就按键,并在选择键集中,按照这个顺序。

happens-before relationJava Language Specification, Chapter 17定义为包括同步-与关系。

即便如此,您应该在附加的对象上正确同步。这是你的目标,这是你的责任。假设只有您的代码在执行程序的线程中写入responseBuffer,并且只有选择器线程在之后从中读取,您说您对写入可用性感兴趣,则您有足够的同步。

什么可能会让你惊讶的是,你从interestOps(...),即使在wakeup()之前获得同步。


从我的经验,如果你有一个很难试图实现通过图书馆事业的正确同步(在这种情况下,选择器),你最好同步对象自己,例如关于对象本身的synchronize声明,ReentrantLock,您在对象操作中使用的其他一些常见同步对象等等。您失去了一点性能(实际上,在大多数情况下,微不足道,假设您不在被守护部分)保持冷静。

3

首先,您不需要通知选择器想要写入。你只是写。只有在写入返回零的情况下,才需要涉及选择器或其线程。

其次,由于选择器的三个同步级别,发生了之前发生的关系,前提是您还进行了同步,如下所示。

如果选择器当前正在选择,您的代码可能会在中调用interestOps()。这种可能性不会被Javadoc排除。您需要按照正确的顺序执行操作:

  1. 唤醒。
  2. 在选择器上同步。
  3. 致电interestOps()

(2)和选择器自身内部同步的组合建立了任何必要的发生之前的关系。

+0

您目前选择_是什么意思?只要'select()'方法调用还没有返回(当它被阻塞或者正在准备其所有的集合时)?当选择器线程被阻塞时,我无法在'interestOps(..)'中重现阻塞在'select()'中。 – Savior

+0

您的建议顺序会通过'synchronized(选择器)'添加发生前的事件。我假设你建议这样做,因为_选择操作按照选择器本身,按键集和选定键集上的顺序同步._正确吗?如果进入阻塞状态,'select'是否会释放它的'Selector'上的锁?这是记录在任何地方还是我们想要承担?如果'select'线程能够在1和2之间重新调用'select',会发生什么情况。 – Savior

+0

'当前选择'表示与''还没有从'select()'返回。选择器在阻塞时不会释放这三个锁:它不能,因为它们是同步,因此它来自您引用的文本。如果它在1和2之间重新调用select(),则2将等到select()返回下一个。 – EJP

相关问题