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

Eri*_*kel 10 language-agnostic multithreading client-server

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

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

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

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

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

我主要担心的是,通过使用这么多线程,我将占用资源.我并不担心竞争条件或死锁,因为这是我无论如何都要处理的事情.

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

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

谢谢!


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

按照我得到的答案(上下文切换是我正在考虑的性能打击,但我不知道,直到你指出它,谢谢!)我想我会采用一个听众,一个方法接收者,一个发送者和一些杂项;-)

Ste*_*owe 7

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

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

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


Dan*_*ker 5

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

每个线程占用1 MB的内存,因此在开始做任何有用的事情之前,你需要占用200MB的页面文件.

通过各种方式将操作分解为可以安全地在任何线程上运行的小块,但是将这些操作放在队列上并且具有为这些队列服务的固定的有限数量的工作线程.

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

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

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


Ben*_*ter 4

我用 .NET 编写,我不确定我的编码方式是否是由于 .NET 限制及其 API 设计,或者这是否是一种标准的处理方式,但这就是我在过去:

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

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

  • 一个连接侦听器线程,侦听传入的连接请求并将这些请求传递给...

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

  • 传入数据侦听器线程,用于侦听当前连接上的传入数据。所有数据都会传递到排队线程以排队等待处理。除了基本侦听和传递数据进行处理之外,您的侦听器线程应该尽可能少地执行操作。

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

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

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

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

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