14

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

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

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

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

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

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

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

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

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

4

5 回答 5

11

如果正确编写响应式应用程序,它们当然可以更好地扩展。这表示

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

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

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

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

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

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

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

于 2013-07-12T13:31:45.700 回答
6

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

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

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

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

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

于 2013-01-14T12:29:11.433 回答
3

FWIW,Poco自版本 1.5.1 起支持选项 2 ( ParallelReactor )

于 2013-02-04T05:09:44.993 回答
1

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

于 2013-01-14T11:59:41.173 回答
1

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

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

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

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

于 2013-01-14T12:26:27.880 回答