代码之家  ›  专栏  ›  技术社区  ›  Erik van Brakel scottrakes

多线程还是尽可能少线程?

  •  10
  • Erik van Brakel scottrakes  · 技术社区  · 16 年前

    作为一个辅助项目,我目前正在为一个我以前玩过的老游戏编写一个服务器。我试图使服务器尽可能松散地耦合,但我想知道对于多线程来说,什么是一个好的设计决策。目前我有以下操作顺序:

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

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

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

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

    我主要关心的是,通过使用这么多线程,我将占用资源。我不担心比赛条件或僵局,因为这是我无论如何都要处理的事情。

    我的设计是这样设置的,我 能够 使用一个线程进行所有客户机通信,不管它是1还是100。我已经将通信逻辑与客户机对象本身分开了,所以我可以实现它而不必重写大量代码。

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

    谢谢!


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

    根据我在这里得到的答案(上下文切换是我考虑的性能冲击,但直到你指出它,我才知道,谢谢!)我想我会用一个听众、一个接收者、一个发送者和一些其他的东西来实现这个方法;-)

    5 回复  |  直到 14 年前
        1
  •  4
  •   BenAlabaster    16 年前

    我在.NET中编写代码,我不确定我编写代码的方式是由于.NET的限制和它们的API设计,还是因为这是一种标准的方式,但这是我过去做过这种事情的方式:

    • 将用于处理传入数据的队列对象。这应该在队列线程和工作线程之间同步锁定,以避免出现争用情况。

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

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

    • 创建连接/会话的连接处理线程。将线程与连接侦听器线程分开意味着您正在减少在该线程处理请求时由于资源减少而丢失连接请求的可能性。

    • 在当前连接上侦听传入数据的传入数据侦听器线程。所有数据都被传递到一个排队线程,以便排队等待处理。您的侦听器线程应该尽可能少地执行基本侦听之外的操作,并将数据传递出去进行处理。

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

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

    这使线程保持了我所认为的简单但健壮的模型。我希望找到一个比这个更简单的模型,但是我发现如果我尝试进一步减少线程模型,我会开始丢失网络流中的数据或错过连接请求。

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

    在TDD环境中,保持每个逻辑任务一个线程的方式与每个任务一个方法的方式相同要简单得多,并且可以在逻辑上区分每个线程应该做什么。发现潜在问题更容易,解决问题也更容易。

        2
  •  7
  •   Steven A. Lowe    16 年前

    使用一个事件流/队列和一个线程池来保持平衡;这将更好地适应具有或多或少核心的其他计算机。

    一般来说,比核心更多的活动线程将浪费时间上下文切换

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

        3
  •  5
  •   Daniel Earwicker    16 年前

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

    每个线程占用1 MB的内存,所以在开始做任何有用的事情之前,您需要占用200 MB的页面文件。

    无论如何,将您的操作分解成可以在任何线程上安全运行的小块,但将这些操作放到队列上,并且为这些队列提供服务的工作线程数量是固定的、有限的。

    更新: 浪费200兆有关系吗?在32位机器上,它占整个进程理论地址空间的10%——没有其他问题。在64位机器上, 声音 就像是理论上可以得到的东西的海洋中的一滴,但实际上它仍然是应用程序毫无意义地保留的一大块(或者更确切地说,大量相当大的块)存储,然后必须由操作系统来管理。它的作用是用大量无用的填充物包围每个客户机的有价值的信息,这会破坏位置,破坏操作系统和CPU将频繁访问的内容保存在最快的缓存层中的尝试。

    无论如何,记忆的浪费只是精神错乱的一部分。除非您有200个内核(以及一个能够使用的操作系统),否则您实际上没有200个并行线程。你有(比如)8个内核,每个内核在25个线程之间疯狂切换。幼稚的您可能会认为,这样做的结果是,每个线程在一个速度慢25倍的核心上运行。但实际上比这更糟糕的是,操作系统花费更多的时间将一个线程从核心上取下,并将另一个线程放到核心上(“上下文切换”),而不是让代码运行。

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

        4
  •  2
  •   Len Holgate    14 年前

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

    其思想是您有少量的线程来处理您的I/O,这使得您的系统能够扩展到大量并发连接,因为连接的数量和为它们提供服务的进程所使用的线程的数量之间没有关系。如预期的那样,.NET将您与详细信息隔离开来,而win32则不会。

    使用异步I/O和这种类型的服务器的挑战在于,客户机请求的处理成为服务器上的状态机,数据到达触发状态更改。有时这需要一些适应,但一旦你做到了,这真的相当了不起;)

    我有一些免费代码,用iOCP演示C++中的各种服务器设计。 here .

    如果你正在使用UNIX或者需要跨平台,你在C++中,那么你可能想看看提供ASI/I/O功能的Boost ASIO。

        5
  •  0
  •   Yuval Adam    16 年前

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

    如果在任何一个特定的时刻只有几个人是活跃的,而其他人都在睡觉或等待什么,那么你就没事了。在这种情况下,休眠线程不会让您付出任何代价。

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