【问题标题】:Many threads or as few threads as possible?尽可能多的线程或尽可能少的线程?
【发布时间】:2010-09-27 09:13:35
【问题描述】:

作为一个副项目,我目前正在为我曾经玩过的一个古老的游戏编写服务器。我试图使服务器尽可能松散耦合,但我想知道多线程的一个好的设计决策是什么。目前我有以下一系列操作:

  • 启动(创建)->
  • 服务器(监听客户端,创建)->
  • 客户端(监听命令并发送周期数据)

我假设平均有 100 个客户,因为这是游戏在任何给定时间的最大值。对于整个事情的线程化,正确的决定是什么?我目前的设置如下:

  • 服务器上的 1 个线程侦听新连接,在新连接上创建一个客户端对象并再次开始侦听。
  • 客户端对象有一个线程,监听传入的命令并定期发送数据。这是使用非阻塞套接字完成的,因此它只是检查是否有可用数据,处理它,然后发送它已排队的消息。登录在发送-接收周期开始之前完成。
  • 一个线程(目前)用于游戏本身,因为我认为从架构上讲,它与整个客户端-服务器部分是分开的。

这将导致总共 102 个线程。我什至正在考虑给客户端 2 个线程,一个用于发送,一个用于接收。如果我这样做,我可以在接收线程上使用阻塞 I/O,这意味着线程在一般情况下大部分是空闲的。

我主要担心的是,使用这么多线程会占用资源。我不担心竞争条件或死锁,因为无论如何我都必须处理这些问题。

我的设计是这样设置的,即我可以对所有客户端通信使用单个线程,无论它是 1 还是 100。我已将通信逻辑与客户端对象本身分离,所以我可以实现它而不必重写很多代码。

主要问题是:在一个应用程序中使用超过 200 个线程是错误的吗?它有优点吗?我正在考虑在多核机器上运行它,这样会充分利用多核吗?

谢谢!


在所有这些线程中,大多数通常都会被阻塞。我预计连接数不会超过每分钟 5 个。来自客户端的命令很少会出现,我会说平均每分钟 20 个。

按照我在这里得到的答案(上下文切换是我正在考虑的性能影响,但在你指出之前我不知道这一点,谢谢!)我想我会选择一个方法监听器,一个接收器,一个发送器,还有一些杂七杂八的东西;-)

【问题讨论】:

    标签: multithreading language-agnostic client-server


    【解决方案1】:

    你的平台是什么?如果是 Windows,那么我建议您查看异步操作和线程池(如果您在 C/C++ 中的 Win32 API 级别工作,则直接查看 I/O 完成端口)。

    这个想法是您有少量线程来处理您的 I/O,这使您的系统能够扩展到大量并发连接,因为连接数和使用的线程数之间没有关系通过为他们服务的过程。正如预期的那样,.Net 将您与细节隔离开来,而 Win32 则没有。

    使用异步 I/O 和这种服务器风格的挑战在于,客户端请求的处理变成了服务器上的状态机,并且到达的数据会触发状态更改。有时这需要一些时间来适应,但一旦你习惯了,它真的很了不起;)

    我有一些免费代码演示了使用 IOCP here 在 C++ 中进行的各种服务器设计。

    如果您使用的是 unix 或需要跨平台并且您使用的是 C++,那么您可能需要查看提供异步 I/O 功能的 boost ASIO。

    【讨论】:

      【解决方案2】:

      简单地回答这个问题,在今天的硬件上使用 200 个线程是完全错误的。

      每个线程占用 1 MB 内存,因此您在开始做任何有用的事情之前就占用了 200 MB 的页面文件。

      通过各种方式将您的操作分解成可以在任何线程上安全运行的小块,但将这些操作放在队列中,并使用固定数量有限的工作线程为这些队列提供服务。

      更新:浪费 200MB 重要吗?在 32 位机器上,它是进程的整个理论地址空间的 10% - 没有其他问题。在 64 位机器上,听起来就像是理论上可用的沧海一粟,但实际上它仍然是一个非常大的块(或者更确切地说,是大量相当大的块)存储被应用程序毫无意义地保留,然后必须由操作系统管理。它的效果是用大量无价值的填充物包围每个客户端的有价值信息,这会破坏局部性,挫败操作系统和 CPU 将经常访问的内容保存在最快的缓存层中的尝试。

      无论如何,内存浪费只是精神错乱的一部分。除非您有 200 个内核(以及能够使用的操作系统),否则您实际上并没有 200 个并行线程。你有(比如说)8 个核心,每个核心都在 25 个线程之间疯狂地切换。您可能会天真地认为,因此,每个线程都相当于在慢 25 倍的内核上运行。但实际上比这更糟糕 - 操作系统花费更多时间从内核中取出一个线程并在其上放置另一个线程(“上下文切换”),而不是实际允许您的代码运行。

      看看任何著名的成功设计是如何解决这类问题的。 CLR 的线程池(即使您不使用它)就是一个很好的例子。它开始假设每个核心只有一个线程就足够了。它允许创建更多,但只是为了确保最终完成设计糟糕的并行算法。它拒绝每秒创建超过 2 个线程,因此它通过减慢线程贪婪算法来有效地惩罚它们。

      【讨论】:

      • 现在 200MB 有那么重要吗?大概这将在一个或多或少专用的服务器上 - 您不会期望在您的工作站上运行 100 个游戏玩家而不注意。
      • 是的,200MB 已经很多了,尤其是如果您还想运行其他东西的话。此外,应用程序的其余部分将需要内存,因此浪费 200Mb 至少可以减少 200MB 的缓存。
      • 谢谢。我想我可以说这是我第一次遇到真正必须考虑这样的资源的情况。我想我选择了一个很好的学习项目,也是因为我很想看到我努力工作的成果;-)
      【解决方案3】:

      使用事件流/队列和线程池来保持平衡;这将更好地适应可能具有更多或更少内核的其他机器

      一般来说,多于核心的活动线程会浪费时间进行上下文切换

      如果您的游戏包含大量短动作,循环/循环事件队列将比固定数量的线程提供更好的性能

      【讨论】:

        【解决方案4】:

        我用 .NET 编写代码,但我不确定我的编码方式是由于 .NET 的限制及其 API 设计,还是这是一种标准的做事方式,但这就是我所做的这种方式过去的事情:

        • 将用于处理传入数据的队列对象。这应该在排队线程和工作线程之间同步锁定以避免竞争条件。

        • 用于处理队列中数据的工作线程。排队数据队列的线程使用信号量通知该线程处理队列中的项目。该线程将在任何其他线程之前自行启动,并包含一个可以运行的连续循环,直到它收到关闭请求。循环中的第一条指令是暂停/继续/终止处理的标志。该标志最初将设置为暂停,以便线程处于空闲状态(而不是连续循环),而无需进行任何处理。当队列中有要处理的项目时,排队线程将更改标志。然后,该线程将在循环的每次迭代中处理队列中的单个项目。当队列为空时,它会将标志设置回暂停,以便在循环的下一次迭代中等待,直到排队进程通知它还有更多工作要做。

        • 一个连接侦听线程,用于侦听传入的连接请求并将其传递给...

        • 创建连接/会话的连接处理线程。拥有一个与连接侦听器线程不同的线程意味着您可以减少由于该线程处理请求时资源减少而丢失连接请求的可能性。

        • 一个传入数据侦听器线程,用于侦听当前连接上的传入数据。所有数据都被传递到排队线程以进行排队处理。您的侦听器线程应尽可能少地完成基本侦听和传递数据以进行处理之外的工作。

        • 一个队列线程,它以正确的顺序对数据进行排队,以便正确处理所有内容,该线程将信号量提升到处理队列,让它知道有数据要处理。将此线程与传入数据侦听器分开意味着您不太可能错过传入数据。

        • 一些在方法之间传递的会话对象,以便每个用户的会话在整个线程模型中都是自包含的。

        这使线程变得像我想出的那样简单但健壮的模型。我很想找到一个比这更简单的模型,但我发现如果我尝试进一步减少线程模型,我会开始丢失网络流上的数据或丢失连接请求。

        它还有助于 TDD(测试驱动开发),这样每个线程都在处理单个任务,并且更容易编写测试代码。拥有数百个线程会很快成为资源分配的噩梦,而拥有单个线程则成为维护的噩梦。

        为每个逻辑任务保留一个线程要简单得多,就像在 TDD 环境中每个任务有一个方法一样,您可以在逻辑上分离每个任务应该做什么。发现潜在问题更容易,解决问题也容易得多。

        【讨论】:

          【解决方案5】:

          我认为您应该问的问题不是 200 作为一般线程数是好是坏,而是这些线程中有多少是活动的。

          如果在任何给定时刻只有其中几个处于活动状态,而所有其他人都在睡觉或等待或诸如此类,那么你就没事了。在这种情况下,休眠线程不会花费您任何费用。

          但是,如果所有这 200 个线程都处于活动状态,那么您的 CPU 将浪费大量时间在所有这大约 200 个线程之间进行线程上下文切换。

          【讨论】:

          • 废话,一个休眠线程有1MB栈,所以200个休眠线程就是浪费了200MB内存。
          • 在一个能够在游戏中管理 100 个客户端的服务器中,200MB 的浪费空间几乎就在噪音之下。
          • @Earwicker - 请引用?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-03-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-03-05
          • 2016-02-04
          • 1970-01-01
          相关资源
          最近更新 更多