【问题标题】:Thread per connection vs Reactor pattern (with a thread pool)?每个连接的线程与反应器模式(带有线程池)?
【发布时间】:2012-12-28 09:16:07
【问题描述】:

我想编写一个简单的多人游戏作为我的 C++ 学习项目的一部分。

所以我想,既然我在做,我想正确地做它而不是仅仅完成它

如果我理解正确:Apache 使用 Thread-per-connection 架构,而 nginx 使用事件循环,然后为传入连接分配一个工作线程 [x]。我猜 nginx 更聪明,因为它支持更高的并发级别。对吧?

我也遇到过这个clever analogy,但我不确定它是否适用于我的情况。这个类比似乎也非常理想主义。我很少看到我的电脑以 100% 的 CPU 运行(即使打开了无数个 Chrome 选项卡,Photoshop 和其他不同时运行的东西)

另外,我遇​​到了一个 SO 帖子(它不知何故从我的历史中消失了),其中一个用户询问他们应该使用多少线程,其中一个答案是拥有大约 700 个甚至高达 10,000 个线程是完全可以接受的线程。不过,这个问题与 JVM 有关。

因此,让我们估计大约 5,000 名用户的虚构用户群。哪种方法应该是“最并发”的?

  1. 在单个线程中运行所有内容的反应器模式。
  2. 带有线程池的反应器模式(大约,您建议线程池应该有多大?
  3. 为每个连接创建一个线程,然后销毁连接关闭的线程。

我承认选项 2 听起来对我来说是最好的解决方案,但我对这一切都很陌生,所以我可能有点天真并遗漏了一些明显的缺陷。此外,这听起来可能很难实现。

PS:我正在考虑使用POCO C++ Libraries。建议任何替代库(如boost)对我来说很好。但是,许多人说 POCO 的库非常干净且易于理解。所以,我最好使用那个,这样我就可以了解我正在使用的如何

【问题讨论】:

    标签: c++ poco-libraries


    【解决方案1】:

    如果编写正确,响应式应用程序肯定会更好地扩展。这意味着

    • 从不阻塞反应线程:
      • 任何阻塞都会严重降低服务器的性能,您通常会使用少量的反应线程,因此阻塞也会很快导致死锁。
      • 没有互斥体,因为它们可以阻塞,所以没有共享的可变状态。如果您需要共享状态,则必须将其包装为参与者或类似对象,以便只有一个线程可以访问该状态。
    • 反应线程中的所有工作都应受 CPU 限制
      • 所有 IO 都必须是异步的或在不同的线程池中执行,并将结果反馈到反应器中。
      • 这意味着使用futures 或callbacks 来处理回复,如果您不习惯并且不遵守纪律,这种代码风格很快就会变得无法维护。
    • 反应线程中的所有工作都应该很小
      • 为了保持服务器的响应能力,reactor 中的所有任务都必须很小(受时间限制)
      • 在 8 核机器上,您不能允许 8 个长任务同时到达,因为在完成之前不会开始其他工作
      • 如果一项任务可能需要很长时间,则必须将其拆分(协作式多任务处理)

    反应式应用程序中的任务是由应用程序而不是操作系统安排的,这就是为什么它们可以更快并使用更少的内存。当您编写响应式应用程序时,您是在说您非常了解问题域,因此您可以更好地组织和安排此类工作,而不是操作系统可以以阻塞方式安排线程执行相同工作。

    我是响应式架构的忠实粉丝,但它们会带来成本。我不确定我是否会将我的第一个 c++ 应用程序编写为响应式,我通常尝试一次学习一件事。

    如果您决定使用响应式架构,请使用一个可以帮助您设计和构建代码的良好框架,否则您最终会遇到意大利面。要寻找的东西是:

    • 工作单位是什么?
    • 添加新作品有多容易?它只能来自外部事件(例如网络请求)吗?
    • 将工作分解成更小的块有多容易?
    • 处理这项工作的结果有多容易?
    • 将阻塞代码移动到另一个线程池并仍然处理结果有多容易?

    我不能为此推荐 C++ 库,我现在在 ScalaAkka 进行服务器开发,它们为所有这些提供了一个出色的可组合期货库来保持代码干净。

    祝你好运学习 C++ 以及你做出的任何选择。

    【讨论】:

    • 如果你在做网络应用,ZMQ有自己的reactor wrapper。 A reference about CZMQ, which is a wrapper of ZMQ这可能是您的选择之一。
    • 这个答案是“反应器设计模式的最佳实践”问题是“反应器与线程池”
    • @Zombies 如果你想学究气,问题是哪个是最多并发的 1) 单线程反应器 2) 带有线程池的反应器 3) 每个连接的线程。这个问题的答案很大程度上取决于你的工作量,是否存在阻塞 IO?请求是否必须协调对状态的访问?国家在变异吗?我清楚地说,正确编写时反应性比例更好。所以答案是 1 或 2。然后给提问者足够的信息,以根据他们的工作量来决定他们是在选项 1 还是 2 之间。同样重要的是,我帮助他们决定是否可以正确实施。
    【解决方案2】:

    选项 2 将最有效地占用您的硬件。这是经典文章,十年了,但仍然很好。

    http://www.kegel.com/c10k.html

    如今,构建具有并发和异步等待的应用程序的最佳库组合是 Boost Thread 和 Boost ASIO。您还可以尝试 C++11 std thread 库和 std mutex (但在很多情况下,Boost ASIO 比互斥锁更好,只是总是回调到同一个线程并且您不需要受保护的区域)。远离std future,因为它坏了:

    http://bartoszmilewski.com/2009/03/03/broken-promises-c0x-futures/

    线程池中的最佳线程数是每个 CPU 核心一个线程。 8 核 -> 8 线程。如果您认为您的线程池线程有时可能会调用阻塞操作,则可能还有一些额外的。

    【讨论】:

    • std future 没有坏了!! C++11 只是缺少一个可以轻松组合它们的功能。换句话说,std future 很好,但彻底有效地使用它们可能,在某些情况下,并不像应有的那么容易。也就是说,无论如何+1:P
    【解决方案3】:

    FWIW,Poco 从 1.5.1 版开始支持选项 2 (ParallelReactor)

    【讨论】:

      【解决方案4】:

      我认为选项 2 是最好的。至于池大小的调整,我认为池应该是自适应的。它应该能够产生更多线程(具有一些较高的硬限制)并在低活动时移除过多的线程。

      【讨论】:

        【解决方案5】:

        正如您链接到的类比(它是 cmets)所暗示的。这在某种程度上取决于应用程序。现在你在这里建造的是一个游戏服务器。让我们分析一下。

        游戏服务器(通常)执行大量 I/O 和相对较少的计算,因此它们远非 100% CPU 应用程序。 另一方面,它们通常也会更改某些数据库中的值(“游戏世界”模型)。所有玩家都创建对该数据库的读取和写入。这正是类比中的交集问题。

        因此,虽然您可能会从在单独的线程中处理 I/O 中获得一些收益,但您也会因为让单独的线程访问同一个数据库并等待其锁定而有所损失。

        所以在您的情况下,选项 1 或 2 都是可以接受的。出于可扩展性的原因,我不推荐选项 3。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2012-09-03
          • 1970-01-01
          • 2011-06-12
          • 2016-10-18
          • 2013-02-19
          • 2013-03-19
          • 2010-12-16
          • 1970-01-01
          相关资源
          最近更新 更多