作为一个侧面项目,我正在为一个我曾经玩过的古老游戏编写服务器。我试图让服务器尽可能松散耦合,但我想知道什么是多线程的好设计决策。目前我有以下一系列行动:
我假设平均有100个客户,因为这是游戏任何给定时间的最大值。对于整个事物的线程,什么是正确的决定?我目前的设置如下:
这将导致总共102个线程。我甚至考虑给客户端2个线程,一个用于发送,一个用于接收。如果我这样做,我可以在接收器线程上使用阻塞I / O,这意味着线程在平均情况下将大部分处于空闲状态。
我主要担心的是,通过使用这么多线程,我将占用资源。我并不担心竞争条件或死锁,因为这是我无论如何都要处理的事情。
我的设计是这样设置的,我可以使用单个线程进行所有客户端通信,无论它是1还是100.我已将通信逻辑与客户端对象本身分开,所以我可以实现它而无需重写大量代码。
主要问题是:在应用程序中使用超过200个线程是错误的吗?它有优势吗?我正在考虑在多核机器上运行它,它会像这样多个核心占用很多优势吗?
谢谢!
在所有这些线程中,大多数线程通常会被阻止。我不认为连接速度超过每分钟5次。来自客户端的命令很少会出现,我平均说每分钟20个。
按照我到达的答案(上下文切换是我正在考虑的性能影响,但我不知道,直到你指出它,谢谢!)我想我会选择一个方法听众,一个接收者,一个发送者和一些杂项; - )
答案 0 :(得分:7)
使用事件流/队列和线程池来维持平衡;这将更好地适应可能具有更多或更少核心的其他机器
一般来说,比核心更多的活动线程会浪费时间上下文切换
如果您的游戏包含大量短操作,循环/回收事件队列将提供比固定数量的线程更好的性能
答案 1 :(得分:5)
简单地回答这个问题,在今天的硬件上使用200个线程是完全错误的。
每个线程占用1 MB的内存,所以在你开始做任何有用的事情之前,你需要占用200MB的页面文件。
通过各种方法将操作分解为可以在任何线程上安全运行的小块,但是将这些操作放在队列上并且有一些固定的,有限数量的工作线程为这些队列提供服务。
更新:浪费200MB的事情?在32位机器上,它占整个理论地址空间的10% - 没有其他问题。在一台64位的机器上,听起来就像理论上可用的海洋一样,但在实践中它仍然是一个非常大的块(或者更确切地说,是大量相当大的块)存储由应用程序无意义地保留,然后必须由OS管理。它具有围绕每个客户端的有价值信息的效果,其中包含大量无用的填充,这会破坏本地性,破坏操作系统和CPU尝试将频繁访问的内容保存在最快的缓存层中。
无论如何,内存浪费只是精神错乱的一部分。除非你有200个内核(以及一个能够使用的操作系统),否则你实际上没有200个并行线程。你有(比方说)8个核心,每个核心在25个线程之间疯狂切换。天真的你可能会认为,由于这个原因,每个线程都会遇到运行速度慢25倍的核心。但它实际上要比这更糟糕 - 操作系统花费更多的时间从核心中取出一个线程并在其上放置另一个线程(“上下文切换”),而不是实际允许代码运行。
看看任何知名的成功设计如何解决这类问题。 CLR的线程池(即使你没有使用它)就是一个很好的例子。假设每个核心只有一个线程就足够了。它允许创建更多,但只是为了确保最终完成设计糟糕的并行算法。它拒绝每秒创建超过2个线程,因此它通过减慢线程贪婪算法来有效地惩罚它们。
答案 2 :(得分:4)
我用.NET编写,我不确定我编码的方式是由于.NET限制和他们的API设计,还是这是一种标准的做事方式,但这就是我这样做的方式过去的事情:
将用于处理传入数据的队列对象。这应该在排队线程和工作线程之间同步锁定,以避免竞争条件。
用于处理队列中数据的工作线程。排队数据队列的线程使用信号量来通知此线程处理队列中的项目。该线程将在任何其他线程之前启动,并包含一个可以运行的连续循环,直到它收到关闭请求。循环中的第一条指令是暂停/继续/终止处理的标志。该标志最初将设置为暂停,以便线程处于空闲状态(而不是连续循环),而不进行任何处理。当队列中有待处理的项目时,排队线程将更改标志。然后,该线程将在循环的每次迭代中处理队列中的单个项目。当队列为空时,它会将标志设置为暂停,这样在循环的下一次迭代中,它将等到排队过程通知它还有更多的工作要做。
一个侦听传入连接请求的连接侦听器线程,并将其传递给...
创建连接/会话的连接处理线程。从连接侦听器线程中获得一个单独的线程意味着,当该线程处理请求时,由于资源减少,您可能会减少错过连接请求的可能性。
传入数据侦听器线程,用于侦听当前连接上的传入数据。所有数据都传递给排队线程以排队等待处理。您的监听线程应该在基本监听之外尽可能少地执行,并将数据传递出去进行处理。
排队线程以正确的顺序对数据进行排队,以便可以正确处理所有内容,此线程将信号量提升到处理队列,让它知道要处理的数据。将此线程与传入数据侦听器分开意味着您不太可能错过传入数据。
在方法之间传递的一些会话对象,以便每个用户的会话在整个线程模型中自包含。
这使得线程变得简单,但是我已经想到了一个强大的模型。我希望找到一个比这更简单的模型,但我发现如果我尝试进一步减少线程模型,我就会开始丢失网络流上的数据或错过连接请求。
它还协助TDD(测试驱动开发),使每个线程处理单个任务,并且更容易编码测试。拥有数百个线程很快就会成为资源分配的噩梦,而拥有一个线程会成为维护的噩梦。
每个逻辑任务保留一个线程的方式与在TDD环境中为每个任务保留一个方法的方式相同,并且您可以在逻辑上区分每个应该执行的操作。它更容易发现潜在的问题,更容易解决它们。
答案 3 :(得分:2)
你的平台是什么?如果是Windows,那么我建议您查看异步操作和线程池(如果您在C / C ++中使用Win32 API级别,则直接查看I / O完成端口)。
这个想法是你有少量的线程来处理你的I / O,这使你的系统能够扩展到大量的并发连接,因为连接数和使用的线程数之间没有关系通过为他们服务的过程。正如所料,.Net将您与细节隔离开来,而Win32则没有。
使用异步I / O和这种类型的服务器的挑战是客户端请求的处理成为服务器上的状态机,并且到达的数据触发状态的改变。有时这需要一些习惯,但一旦你这样做真的相当奇妙;)
我有一些免费代码,使用IOCP here演示了C ++中的各种服务器设计。
如果您正在使用unix或需要跨平台而且您使用的是C ++,那么您可能需要查看提供异步I / O功能的boost ASIO。
答案 4 :(得分:0)
我认为你应该问的问题不是200作为一般线程数是好还是坏,而是这些线程中有多少是活跃的。
如果在任何特定时刻只有几个人活跃,而其他所有人都在睡觉或等待或等等,那么你没事。在这种情况下,睡眠线程不需要任何费用。
但是,如果这200个线程中的所有线程都处于活动状态,那么您的CPU将浪费这么多时间在所有这200个线程之间进行线程上下文切换。