Thread.Yield()会导致CPU占用过高?

3
在我的一个项目中,我注意到随着连接的客户端数量增加,服务器的CPU使用率急剧上升。

10个客户端:大多数情况下为0%,偶尔会突然上升到7%。
15个客户端:大多数情况下为0%,偶尔会突然上升到10%。
25个客户端:大多数情况下为10%,偶尔会突然上升到60%。
50个客户端:大多数情况下为50%,偶尔会突然上升到60%,由于游戏服务器,CPU总体上达到100%。(注:CPU上有8个逻辑核心)

我将问题缩小到 Thread.Yield 这一行上:https://github.com/vercas/vProto/blob/master/vProto/Base%20Client/Package%20Sending.cs#L121 一旦我注释掉这一行,即使有100个客户端,CPU使用率也会始终保持在0%!
为什么Thread.Yield会这样做?

在我的测试中并没有实际的发送操作。交换几个指针不可能那么耗费资源... - Vercas
我不怀疑它与调度有某种关系,但我怀疑它并不仅仅是因为上下文切换。我真的认为它可能与锁争用或调度程序的其他退化情况有关。Thread.Sleep(1)是否也会出现类似的峰值?如果没有,那么吞吐量呢? - user2864740
我可以轻松地进行测试,只需将休眠放置在前一个if语句(检查是否有任何排队的包)附加的else语句中即可。至于吞吐量,每个客户端(和每个线程)每30秒只发送一个无主体包。现在存在相同的吞吐量,并且没有展示出相同的问题。 - Vercas
是指通过移除锁定或仅使用Thread.Sleep(1),还是两者结合使用? - user2864740
Thread.Sleep(1)。有1200个客户端,有时只到1%。我正在通过朋友的帮助进行基准测试。到目前为止,结果非常令人满意。对于我来说,ping值约为70。这比我在游戏服务器上得到的还要少(在同一台机器上)。 - Vercas
显示剩余2条评论
2个回答

2
我不知道为什么使用Thread.Yield/Sleep会导致这些峰值,但我反驳它仅仅是由于“上下文切换”引起的。(我毫不怀疑它与上下文切换有关,但需要更强的解释。)
当使用Yield和Sleep时,Thread.Sleep或Thread.Yield似乎给出了一个令人满意的答案 - 基本上是Yield,像Sleep(0)一样,可能不会产生结果 - 尽管它可能不直接适用于“如果需要,则使用Yield和Sleep”1与“总是休眠而不尝试产生Yield”的情况,如本问题所述。 1原始的CPU峰值代码使用:if (!Thread.Yield()) Thread.Sleep(10);。(这是为什么在问题中包含相关代码很重要的一个例子。)
我反驳上下文切换是问题的原因的论据如下。
  1. Windows使用抢占式调度,即使线程不主动让出,也会进行数十次上下文切换。
  2. Thread.Sleep(x),其中x> 0,将始终导致上下文切换;然而,据报道Thread.Sleep(1)不会引起这样的峰值。
  3. Thread.Yield可能不会导致上下文切换,但据报道会引起峰值。

    操作系统(即:Thread.Yield)如果不..


正如我之前提到的,即使每30秒只发送两个数据包,当客户端数量达到300个时,CPU使用率也会呈指数级增长。在删除Thread.Yield()后,该文件的所有更新都没有出现过这种情况,即使在负载下也是如此。特别是新的基于线程池的方法,真是太棒了。在连接了大约1800个vProto客户端到1个vProto服务器以及连接到同一台服务器机器上的游戏服务器的大约250个客户端之前,我已经饱和了网络连接,但CPU使用率仅为2%。尽管如此,我仍然希望能够找到这个问题的根本原因。 - Vercas

1
由于Thread.Yield的释放处理方式,它强制当前进程线程过早地释放。这反过来向所有其他进程发送消息,告诉它们去做自己的事情。在交换内存、加载缓存进程和按顺序移动进程列表方面,切换进程上下文是昂贵的。
来自 MSDN
如果此方法成功,则其余线程的当前时间片将被释放。操作系统根据其优先级和可运行的其他线程的状态,安排调用线程的另一个时间片。
屈服仅限于执行调用线程的处理器。即使该处理器处于空闲状态或正在运行低优先级的线程,操作系统也不会将执行切换到另一个处理器。如果没有其他准备好在当前处理器上执行的线程,则操作系统不会屈服执行,并且此方法返回false。
此方法相当于使用平台调用来调用本地Win32 SwitchToThread函数。您应该调用Yield方法,而不是使用平台调用,因为平台调用绕过了主机请求的任何自定义线程行为。

更新

对于Thread.Yield会导致昂贵的上下文切换的说法存在一些争议。以下是其他参考资料:

Thread.Sleep0和Thread.Yield之间的区别

C#中的线程 - Joseph Albahari

MSDN - 关于进程和线程

MSDN - 多任务考虑因素

建议指南是尽可能少使用线程,从而最小化系统资源的使用。这可以提高性能。多任务处理具有资源需求和潜在冲突,在设计应用程序时需要考虑。资源需求如下:
  • 系统会为进程和线程所需的上下文信息消耗内存。因此,可创建的进程和线程数量受可用内存的限制。
  • 跟踪大量线程会消耗大量处理器时间。如果线程太多,大部分线程将无法取得重大进展。如果当前大部分线程在一个进程中,则其他进程中的线程调度频率较低。

我希望我能够窥视mscorlib.dll中的Thread.YieldInternal! - Vercas
另外,由于Thread.Sleep(10)会导致“昂贵的切换进程上下文”(尽管它会说“在10毫秒之前不要再调用我”),那么为什么仅当未使用Thread.Sleep(10)时,只有Thread.Yield受到这些峰值的影响呢?也就是说,我对“切换进程上下文”导致这些峰值的说法提出了质疑,因为在两种情况下都会发生。 - user2864740
当.NET调用Win 32线程切换时,我相当确定这样做也会暂停.NET运行时。如果有一种方法只是暂停/让出给其他.NET线程,它将减少很多交换。 - Adam Zuckerman
线程休眠至少有两种处理路径。如果您执行sleep(0),它会执行与yield完全相同的操作。如果您放置一个大于0的数字,则会运行不总是执行进程切换的不同代码路径。 - Adam Zuckerman
2
你似乎混淆了两个非常不同的概念 - 上下文切换和内存交换。上下文切换并不一定需要进行内存交换,因此你回答的推理似乎是错误的。 - Iridium
显示剩余7条评论

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