【问题标题】:Multiple selectors in multiple threads多个线程中的多个选择器
【发布时间】:2014-03-03 13:18:11
【问题描述】:

在 n 个线程之间分配传入连接是否有益,每个线程都有自己独立的 NIO Selector,其中 n 是服务器中的内核数?假设我正在编写一个服务器,它应该处理多个客户端连接。我可以有类似的东西:

selector.select();
Iterator<SelectionKey> i = selector.selectedKeys().iterator();

while (i.hasNext()) {
   SelectionKey key = i.next();
   i.remove();

   if (!key.isValid())
      continue;

   if (key.isAcceptable()) {
      // get one of the n selectors (I'd have one per core)
      Selector chosenSelector = getRandomSelector();

      // delegate the new connection to the chosen selector
      SocketChannel newChannel = key.channel.accept();
      newChannel.configureBlocking(false);
      newChannel.register(chosenSelector, SelectionKey.OP_READ);
   }
}

你们觉得这有道理吗?我的意思是,运行 n 个线程,每个线程都有不同的选择器?还是我应该坚持使用一个选择器线程来处理所有连接的 OP_READ?还是别的什么?

【问题讨论】:

  • 保持不同客户端之间的消息顺序已经够难的了,无需刻意让它变得更难。你打算如何完成它?时间戳?
  • 我猜排序消息是一个正交的问题......一旦我收到它们,有人(一个线程)检查时间戳并确保它们以正确的顺序处理,没有间隙。但我想我在这里问的是更基本的东西。

标签: java multithreading selector nio


【解决方案1】:

不,这没有好处,因为需要处理的代码与 IO 操作所需的时间之间的关系可以忽略不计。特别是如果您考虑同步碎片数据所需的额外时间。然而,在单独的线程中处理接收到的数据是有益的。

所以基本上:有一个单线程选择器循环,将数据从一个缓冲区复制到任务缓冲区中,以便在单独的线程中进一步处理,然后在 Executor 中启动带有该任务缓冲区的 Runnable 以处理复制的数据.

【讨论】:

  • 如果每个线程都获取可读键,反序列化消息,然后将反序列化的对象排入单个消息队列以供服务器处理怎么办?假设这样的队列实际上是必要的,那么多线程选择不是有用吗?
  • 你需要从你的网卡角度来看这个:你只有一张,所以线程无法加快从一个数据源读取的速度。另外主线程需要等到所有读/写操作都执行完毕,因为键需要在下一次选择之前完成,所以这里也没有任何收获。总之,您将拥有一个速度低于单线程的复杂解决方案。如果要使用线程,请在channel.read 之后开始,但不要在之前。
  • 因为这也让我有点困扰,所以我确实编写了一个测试程序来测量选择循环实际在做什么,结果证实了我的建议。虽然在最大负载下循环处理约 90k IOs/秒,但它仍然只处理大约 1.5 个键/选择循环。所以线程循环不会有什么好处。
  • 这是一个很好的实验。您是在每个选择器线程中反序列化您的消息,还是只是将字节数组/ByteBuffer 原样交给您的应用程序?
  • 数据被复制到选择器线程中的任务缓冲区中,但该数据的实际处理是在后台线程中完成的。所以循环 >90% 忙于进行选择。
【解决方案2】:

这正是 Netty 所做的。它使用 N 个线程调用 Selector.select() 其中 N = (num available CPUs * 2) 由于超线程,它乘以 2。这是父事件循环,它被认为是接受套接字连接的“接受者事件循环”。 然后是线程工作者的子EventLoop

建议你看看NioEventLoopGroup-class

【讨论】:

  • 能否请您提供一些参考(github 链接)到“它使用 N 个线程调用 Selector.select()”?
【解决方案3】:

在 n 个线程之间分配传入连接是否有益,每个线程都有自己独立的 NIO 选择器,其中 n 是服务器中的内核数?

所有主要框架都以这种方式运行; Mina、Netty、Nginx 等都在多个选择器之间分配套接字。 Nginx 确实将文件描述符传递给具有自己的选择器的分叉进程。

您在这里描述的是基本的选择器-工作者模型。每个核心通常有 2 个工人。每个工人都有一个Selector。这个模型在 Windows 上是绝对必要的,因为每个 Selector 只能处理 1024 个套接字,而 JDK 所做的解决方法对性能来说是灾难性的。

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/sun/nio/ch/WindowsSelectorImpl.java

  • 核心 0 - 线程 -> 选择器 -> OP_READ/OP_WRITE -> 执行业务逻辑
  • 核心 1 - 线程 -> 选择器 -> OP_READ/OP_WRITE -> 执行业务逻辑
  • 核心 2 - 线程 -> 选择器 -> OP_READ/OP_WRITE -> 执行业务逻辑
  • 核心 3 - 线程 -> 选择器 -> OP_READ/OP_WRITE -> 执行业务逻辑

这是缩放反应器模式的基本方法。

重要的是单个SocketChannel 仅附加到单个Selector。所以不要打电话给SocketChannel.register() 给一堆SelectorsOP_READ,因为你不会得到任何好处。

http://man7.org/linux/man-pages/man7/epoll.7.html

Q2 两个epoll实例可以等待同一个文件描述符吗?如果 那么,事件是否同时报告给两个 epoll 文件描述符?

A2 是的,事件会同时报告给双方。不过,小心 可能需要编程才能正确执行此操作。

【讨论】:

  • 没有SocketChannel.attach()这样的方法。这里没有“基本反应堆模式”的证据。什么是“制作冰沙”?引用的最后一句话是什么?
  • 是的,它是 SocketChannel.register - 我编辑了回复。对于请求响应情况,使用此模式可以大大减少缓存未命中。我的理解是 Mina 和 Netty 都以这种方式工作,但我必须稍后验证。
【解决方案4】:

我将只有一个选择器,并让它通过无锁环形缓冲区将消息分配给固定数量的线程。然后,您可以以完全无锁定、超快速的方式进行流程。流程是这样的:

关键选择器 => DEMUX => 工作线程 => MUX => 关键选择器

您只需要确保您的工作线程数量足够(并且您有足够的空闲内核供它们使用)来快速处理您的消息,否则您最终可能会得到一个完整的 DEMUX,并且选择器将不得不阻塞或丢弃消息。

我建议您阅读this article 以了解异步、单线程、非阻塞网络框架的工作原理。当然你也可以查看 Netty 或 Mina。有关使用 demux 和 mux 支付的延迟(以纳秒为单位)的概念,请参阅 this benchmark

【讨论】:

  • “异步”和“非阻塞”不是同义词。您不需要同时需要它们。它们指的是两种不同的 I/O 模型,阻塞是第三种。 Java 已经内置了这样的库。
  • @EJP 我没有说或认为它们是同义词。我认为异步和非阻塞是两个不同的东西,它们齐头并进。如何在不阻塞的情况下异步?或许你可以澄清一下?我知道 java 有 java.nio 但它是选择器的基础。然后在它上面有 Netty、Mina、CoralReactor 和其他框架。顺便说一句,我编辑了我的答案,以包括您使用 demux / task-buffer 的建议。我完全同意。
  • 您的文章与异步 I/O 没有任何关系。 “非阻塞”表示操作立即完成或失败。 “异步”意味着操作在后台继续,而您调用的实际异步 I/O 方法返回到您的代码。他们甚至没有一点关系。 Java NIO 自从我认为 1.7 以来,除了非阻塞和多路复用之外,还有一大堆异步通道。
  • @EJP 调用同步代码会阻塞当前线程,直到所述代码完成,同时调用相同的代码并将其放在线程池中会立即返回给调用者,因此不是 blocking 当前线程.这种区别让人们感到困惑,尽管我认为@rdalmeida 知道他们在说什么,只是在概念上与你有所不同。
猜你喜欢
  • 2012-03-17
  • 2010-11-09
  • 1970-01-01
  • 2012-02-10
  • 2017-06-10
  • 1970-01-01
  • 2021-05-20
  • 2011-07-14
  • 2011-03-30
相关资源
最近更新 更多