C# - 在高活跃服务器中何时使用标准线程、线程池和TPL?

28

最近我一直在阅读关于线程的文章,因为我想开发一个高性能、可扩展的TCP服务器,可以处理高达10,000-20,000个客户端,并且每个客户端都会与服务器进行双向通信,使用基于命令的系统。服务器将接收到命令,并根据命令执行单个(或多个)任务。我的问题是如何适当地利用.NET线程构造函数处理各种情况,执行可能需要1分钟到数小时不等的任务,这取决于所执行的工作。

最混乱的是,无论我读到哪里,都会看到类似于“使用手动创建的线程(或自定义线程池)来处理“长时间运行”的任务,并使用TPL处理短期任务,或需要并行处理的任务。” 什么是“长时间运行”的任务呢?是5秒、60秒还是一个小时?

在使用以下三种方法创建线程的时间范围内,我应该使用哪种方法:

  • 手动创建的线程
  • .NET ThreadPool 类
  • TPL

我考虑的另一个问题是——假设我的服务器实际上连接了20,000个客户端,每个客户端每秒发送1个命令(可能转换为一个或多个任务)。即使使用强大的硬件,我是否有可能将过高的工作负载推入我的线程池/工作项队列中,因此最终会在队列慢慢填充到最大值后生成OutOfMemoryException?

任何见解都将不胜感激。


每个命令平均需要多长时间才能被处理? - rene
这就是问题所在——真的无法确定命令完成需要多长时间。我感觉已经收到足够的信息来做出相应的计划。当然,2万个客户可能有些困难(5000个更为现实),但如果未来需要扩展,我想做好准备。谢谢大家的回复。 - cjones26
2
@slashp 长时间运行意味着“超过几百毫秒”,即会干扰您的其他工作(并引入显著的处理延迟)。如果您的目标是5毫秒的延迟,那么即使1毫秒也可以被认为是“长时间运行”:D 另外,我想指出,20k个TCP客户端接近实际限制 - 每个TCP连接需要一个单独的端口,而您最多只有65535个 - 并且它们在关闭后大约存活4分钟。您可能需要考虑扩展(更多服务器)而不是扩展(每个服务器更多连接)。 - Luaan
4个回答

18

实际上,在这种情况下,所有这些都是次要的;您应该首先看一下异步IO,也就是.BeginRead(...)等方式;这可以通过等待IO完成端口来最小化线程数 - 更加高效。

一旦您有了完整的消息,在那个规模下,我会将消息投入定制的线程池/同步队列中。我会有一定数量的常规线程(而不是池线程或IOCP)服务于该队列以处理每个项目。

事实上,我目前正在做类似的事情(较低规模);为了防止内存暴增,我限制了工作队列的大小;如果它变满了(即工作者跟不上),则您可能会短暂地阻止IOCP,最终使用超时告诉客户端“太忙了”在IOCP层。


+1 对于“队列 + 服务线程”的做法非常有可能是最佳选择。 - Adam Ralph
1
抱歉,我之前忘记提到网络部分会使用IOCP技术,我只是想提供一些我想要实现的背景。我也知道在使用IOCP时需要注意堆碎片问题,它可能导致OutOfMemoryException异常。我真正需要的是关于何时使用这三种结构以及“长时间运行”任务的定义的澄清。 - cjones26
@slashp 我编辑了一些关于内存问题的想法;由于你可能会持续运行,最好自己拥有线程 - 避免与线程池混淆,而且你可以为它们命名。 - Marc Gravell

10

让我最困惑的是,在我阅读到的所有地方,都会看到类似于“使用手动创建的线程(或自定义线程池)来处理‘长时间运行’的任务,并使用TPL来处理短暂的任务或需要并行处理的任务”的建议。

这个建议有些奇怪,或者你可能引用错了。线程也可以进行并行处理,使用TPL时,你可以创建一个带有LongRunning选项的Task。不过,你应该避免在ThreadPool上启动长时间运行的任务。

什么是长时间运行的任务?是5秒、60秒还是1小时?

TPL在ThreadPool之上运行,ThreadPool将以每秒最大2个线程的速度创建新线程。所以,长时间运行就是>= 500毫秒。


即使使用强大的硬件,我是否存在将过高的工作量推入任何线程池/工作项队列的风险?

是的,没有任何线程工具可以扩展你的实际容量...

当你有20k个客户端时,你可能需要一个服务器农场,这是你早期设计中要考虑的一个选项...

因此,在深入研究套接字之前,你应该好好看看WCF。


现在我认为线程调度程序不总是尊重“LongRunning”选项?感谢有关“长时间运行”的信息。 - cjones26
@slas LongRunning是调度程序的提示,所以我想它可以自由地忽略它,“没有LongRunning”的整个事情是关于(不)打扰调度程序/负载平衡的。 - H H
1
尽管LongRunning被定义为一种提示,但默认的TaskScheduler实现总是会创建一个新的线程 - 请参阅http://coderkarl.wordpress.com/2012/12/13/long-running-tasks-and-threads/。 - Lummo
@HenkHolterman 你怎么知道ThreadPool在每秒最多创建2个新线程?它在哪里可以配置? - Alexander Vasilyev

8

我会按照Marc的建议执行。但是如果您的任务需要超过一秒钟,而客户端每秒发送一个请求,则队列将稳定增加。

在这种情况下,我会使用一个服务器作为门面,该服务器获取所有来自客户端的请求,并以异步方式向它们发送响应。

服务器将把所有请求放在一个消息队列中,由几个其他服务器读取。这些服务器处理请求并将响应放在另一个消息队列中,由第一个服务器读取。

另一种解决方案是使用负载均衡服务器。


4

您似乎正在构建一个服务器,该服务器将为数千个并发请求提供服务,每个请求的持续时间长达几分钟到几小时。

通常情况下,让线程的工作负载在几秒钟内完成。任何超过这个时间的操作都会占用服务器资源,并严重影响服务器的可扩展性。如果有成千上万的线程在长时间运行的操作上阻塞,或者同时执行这些长时间运行的操作,那么您的可扩展性肯定会受到影响。

不确定每个长时间运行的操作消耗多少CPU时间。这将影响您的设计,例如:

如果每个长时间运行的操作主要是在I/O上阻塞,您可以使用一个线程等待重叠的I/O或I/O完成端口,然后唤醒新线程来处理已完成的I/O(最多限制数量)。您需要有一个限制线程数来服务等待连接。

如果每个长时间运行的操作等待其他操作完成,请考虑使用Windows Workflow Foundation。

如果每个长时间运行的操作消耗CPU,您不希望同时运行太多此类操作,否则它会使您的服务器陷入困境。在这种情况下,请使用MSMQ和/或TPL来排队任务,并确保只有少数任务同时运行。

在所有这些情况下,似乎您正在保持客户端连接处于打开状态。最糟糕的做法是为每个连接保持一个线程阻塞。您需要实现线程池策略,以仅使用有限数量的线程来服务所有未完成的连接。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接