许多线程或尽可能少的线程?

许多线程或尽可能少的线程?

问题描述:

作为一个副项目,我正在编写一个我曾经玩过的古老游戏的服务器。我试图让服务器尽可能松散耦合,但是我想知道什么是多线程优秀的设计决策。目前,我有行动的顺序如下:许多线程或尽可能少的线程?

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

我假设平均有100个客户,因为这是游戏任何时间的最大值。对于整个线程的线程,什么是正确的决定?我目前的设置如下:监听新的连接,新的连接创建一个客户端对象,并再次启动监听服务器上

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

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

我主要关心的是,通过使用这么多线程,我会占用资源。我不担心竞赛状况或僵局,因为这是我必须处理的事情。

我的设计的设置方式是我的可能使用一个单线程的所有客户端通信,无论它是1还是100.我已经将通信逻辑从客户端对象本身分开,所以我可以在不必重写大量代码的情况下实现它。

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

谢谢!


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

按照我在这里得到的答案来回答(上下文切换是我正在考虑的性能打击,但直到您指出它时我才知道,谢谢!)我想我会去的方式有一个监听器,一个接收器,一个发送器,以及其他一些东西;-)

我写在.NET,我不知道,如果这样,我的代码是由于.NET的限制和他们的API设计或者如果这是一个做事的标准方法,但是这是怎么了,我做到了这一点过去的一些事情:

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

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

  • 一个连接监听线程监听传入的连接请求并把这些关闭以...

  • 创建该连接/会话的连接处理线程。从连接监听器线程中分离线程意味着您可以减少由于线程处理请求时资源减少而导致连接请求丢失的可能性。

  • 传入数据监听线程侦听当前连接上接收的数据。所有数据都传递给排队线程以排队等待处理。您的监听线程应尽可能少地进行基本监听并将数据传递给处理。

  • 排队线程按照正确的顺序将数据排队等候所有事情都可以正确处理,该线程将信号提升到处理队列,让它知道有数据需要处理。将此线程与传入数据侦听器分开意味着您不太可能错过传入数据。

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

这样可以保持线程简单,但是像我想象的那样稳健。我很想找到一个比这更简单的模型,但是我发现如果我尝试并进一步减少线程模型,那么我开始丢失网络流上的数据或错过连接请求。

它还协助TDD(测试驱动开发),使每个线程处理单个任务,并且更容易编码测试。拥有数百个线程可能很快成为资源分配的噩梦,同时拥有单个线程成为维护的噩梦。

在每个逻辑任务中保持一个线程的方式要简单得多,就像在TDD环境中每个任务有一个方法一样,并且您可以在逻辑上区分每个应该做什么。发现潜在问题更容易,修复它们也更容易。

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

一般

其他机器,更多的活动的线程比你的核心将时间浪费在上下文切换

如果你的游戏是由很多短的行动中,圆形/回收事件队列会提供更好的性能比固定数量的线程

我想你应该问的问题不是200作为一般的线程数是好还是坏,而是如何将这些线程的多是将变得活跃。

如果只有几个人都活跃在任何给定的时刻,而所有其他正在睡觉或等待或诸如此类的东西,那么你的罚款。在这种情况下,沉睡的线程不会让你付出任何代价。

但是,如果所有的200个线程是活跃的,你将有你的CPU浪费了这么多时间做线程上下文的所有〜200个线程之间切换。

+0

废话,睡眠线程有一个1MB的堆栈,所以200个睡眠线程是200MB浪费的内存。 – 2008-12-17 18:21:33

+0

在一台能够管理游戏中100个客户端的服务器中,200MB的浪费空间几乎就在那里。 – 2008-12-17 18:37:19

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

这个想法是,你有少量的线程处理你的I/O,这使得你的系统能够扩展到大量的并发连接,因为连接数和线程数之间没有关系由服务于他们的进程使用。如预期的那样,.Net将你从细节中解脱出来,而Win32则不会。

使用异步I/O和这种服务器风格的挑战是,客户端请求的处理成为服务器上的状态机,数据到达触发状态更改。有时候,这需要一些习惯,但一旦你做了它真的很奇妙;)

我有一些免费的代码,演示使用IOCP的C++的各种服务器设计here

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

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

每个线程都占用1MB的内存,所以在开始做任何有用的事情之前,你需要占用200MB的页面文件。通过一切手段将您的操作分解为可以在任何线程上安全运行的小块,但将这些操作放在队列中并且具有固定的有限数量的服务于这些队列的工作线程。

更新:浪费200MB是否有用?在32位机器上,它是整个理论地址空间的10% - 没有其他问题。在64位机器上,它听起来像是在理论上可用的海洋中的一滴水,但实际上它仍然是一个非常大的块(或者说,大量相当大的块)的存储被毫无意义地保留通过应用程序,然后必须由OS管理。它具有将每个客户的有价值信息与大量无价值的填充信息包围起来的作用,这些信息破坏了局部性,破坏了操作系统和CPU将频繁访问的东西保存在最快的缓存层中的企图。

无论如何,内存浪费只是疯狂的一部分。除非你有200个内核(和一个可以使用的操作系统),否则你实际上并没有200个并行线程。你有(说)8个核心,每个核心疯狂地切换25个线程。天真地你可能会认为,由于这个原因,每个线程的体验相当于运行速度慢25倍的核心。但实际上这比它糟糕得多 - 操作系统花费更多的时间将一个线程从核心转移到另一个线程上(“上下文切换”),而不是实际上允许代码运行。

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