线程在同步中所消耗的时间过高吗?

10

今天我使用Visual Studio 2010性能分析器对我的C#应用程序进行了分析。具体来说,我正在针对"并发性"进行分析,因为似乎我的应用程序应该具有比它展示的更多容量。分析报告显示线程大约花费了70-80%的时间处于同步状态。

说实话,我不确定这是什么意思。这是否意味着应用程序正在遭受活锁条件?

为了背景...有~30个以上长时间运行的线程绑定到单个AppDomain (如果这很重要的话),一些线程非常忙碌(例如while(true) { _waitEvent.WaitOne(0); //do stuff })。

我意识到这是一个相当模糊的问题...我想知道关于线程同步状态的含义的一些启示。太多了有多少才算过多?为什么? ~75%真的很糟糕吗?我是否有太多的线程?还是我应该开始在其他领域寻找答案?


1
我的水晶球显示,您拥有一台具有8个CPU核心的机器。因此,您始终有22个线程在等待运行的机会。22/30 * 100% = 73%空闲。您需要更多硅片。 - Hans Passant
你的程序是做什么的?你使用线程解决了哪些瓶颈问题? - Brian
5
如果线程在线程性能报告的“同步”部分中显示,那么它们被阻塞在一种与磁盘、网络、UI消息泵、页面故障、其他更高优先级线程抢占或故意休眠线程无关的方式中。处于同步类别中很可能是由于某种互斥锁的争用;我怀疑问题不是由于处理器饱和而引起的。相反,可能恰恰相反;我愿意打赌,在Joe的情况下,处理器利用率很低。 - Eric Lippert
1
是的,很可能。我的水晶球这个星期有点不准。 - Hans Passant
Eric,你是正确的...我的CPU利用率相当低。 - JoeGeeky
显示剩余6条评论
3个回答

20
我不确定这是什么意思。
这意味着线程平均花费了75%的时间等待另一个线程完成某些工作。
这是否意味着应用程序正遭受活锁状态?
也许!
要为不熟悉术语的读者澄清:'死锁'是指两个线程都在等待彼此完成,因此它们永远等待。'活锁'是一种情况,其中两个线程试图避免死锁,但由于其选择不佳,仍花费大部分时间等待。例如,想象一张桌子上有两个人、一把叉子和一把刀。两个人都希望拿起这两个餐具,使用它们,然后放下。假设我拿起刀子,你拿起叉子。如果我们都决定等待对方放下餐具,我们就会陷入死锁。如果我们都意识到自己即将进入死锁状态,并且我放下刀,你放下叉子,然后我拿起叉子,你拿起刀子,我们就被活锁住了。我们可以无限重复这个过程;我们都在努力解决这个问题,但我们没有有效地沟通以快速解决它。
但是,我猜测你不处于活锁状态。我的猜测是,你只是在少数关键资源上有巨大的竞争,这些资源一次只能由一个线程访问。奥卡姆剃刀原理表明,你应该假设简单的假设 - 大量线程轮流使用稀缺资源 - 而不是复杂的假设 - 一堆线程都试图告诉对方“不,你先去”。
有约30多个长时间运行的线程绑定到单个AppDomain(如果有必要),其中一些线程非常忙碌(例如while(true){_waitEvent.WaitOne(0); // do stuff})。
听起来很糟糕。
我意识到这是一个相当模糊的问题。
是啊,它是这样的。

太多是多少?为什么?

好吧,假设你正在尝试开车穿越城镇,你和城市中的每个司机都花费了75%的时间等待其他司机在交通灯前停留。你告诉我:这太多了吗?为什么?对于一些人来说,在交通拥堵中度过一小时以行驶15分钟的路程可能是完全可以接受的,但对于其他人来说则是完全无法接受的。每当我在高峰时间驾车经过SR 520时,我都要花费一个小时的时间才能行驶应该只需15分钟的路程;我不能接受这种情况,所以现在我坐公交车。

无论这种糟糕的性能对您和您的客户来说是否可以接受,都由您决定。解决性能问题是很昂贵的。您应该问的问题是,通过承担诊断和解决问题的费用,您将获得多少利润。

约75%真的很糟糕吗?

您的线程所需的时间比实际需要的时间长了四倍。在我看来并不好。

我线程数量太多了吗?

几乎可以肯定,是的。30个线程很多。

但在您的情况下,这完全是一个错误的技术问题。问“我是否有太多线程?”就像试图通过问“这个城市的车太多了吗?”来解决交通拥堵问题一样。正确的问题应该是“为什么这个城市有这么多红绿灯而不是高速公路?” 问题不在于线程;问题在于它们互相等待而不是不停地驶向目的地。

我应该开始在其他领域寻找吗?

我们怎么知道呢?


3
我仍在思考你的回答... 但说实话,我欣赏你的直率,并在阅读时大声笑了。谢谢... 表述得很好! - JoeGeeky
1
@JoeGeeky:既然你已经打开了分析器,我建议你花些时间使用它来了解为什么你的线程在相互等待上花费了这么多时间。看一下这个页面:http://msdn.microsoft.com/en-us/magazine/ee336027.aspx -- 滚动到“Inter-thread Dependencies”标题下面,它会给你一些关于如何使用可视化工具的建议,告诉你哪些线程正在等待哪些锁。祝你好运;这类问题很难调试。 - Eric Lippert
谢谢这篇文章,我正在阅读中...需要一点时间来消化。乍一看,高同步数本身可能并不是一件坏事?我想我需要回去看看我的抢占数字等等... - JoeGeeky
2
@JoeGeeky:高同步数表明程序没有有效利用多核机器的处理能力。这是一件坏事吗?这取决于你的客户是否会接受,比如说他们购买了一台四核机器,但只得到了单核机器的性能表现。大多数客户期望购买更多的核心将为他们带来更好的性能;你可能会让他们失望。 - Eric Lippert
当然,这也取决于你的程序具体是做什么。如果你的程序是一个计算器,用4毫秒而不是1毫秒去计算某些东西并不是问题。但如果它是一个编译器,有人坐在那里等待它完成编译,他们可能买电脑的目的就是为了使编译速度更快,所以如果需要4分钟而不是一分钟,他们会非常失望。 - configurator
显示剩余3条评论

2
不了解程序结构,我只能告诉你“同步”在线程中的意义。我无法确定程序中的问题所在。
“同步”基本上是指在共享资源被多个线程访问时,为了避免数据损坏,需要“协调”线程的并发运行。例如,如果您有一个字符串,两个线程都要写入它,如果没有线程同步(例如使用AutoResetEvents或Semaphores等),则一个线程可能正在修改该字符串,被操作系统中断,而第二个线程开始从这个字符串读取,此时该字符串处于不确定状态。这将对您的程序造成严重影响,因此为了避免这种情况以及由线程引起的许多其他可能的错误,需要通过“锁定”来“同步”对共享资源的访问,这样一次只能有一个线程写入/读取它,任何其他想要这样做的线程必须等待第一个线程释放它持有的锁。
这是非常简化的线程同步的含义和作用。实际上,还有许多其他与线程相关的内容,但这是一本完整书籍的主题。
至于“线程同步状态”可能意味着什么,我猜测它意味着许多线程花费时间等待其他线程持有某些共享资源。实质上,这意味着您的程序实际上不是并发工作的,而是以串行方式执行操作,因为线程花费时间“等待其他线程”。这意味着程序编写得不够好,无法真正实现并发处理。当然,这并不是一件容易的事情,具体情况取决于具体情况。
希望这可以帮助您。

根据您最后一段的陈述(例如,“这实际上意味着您的程序并没有真正同时工作,而是按顺序执行任务”),这可能是因为在目标环境中可用的处理器(和/或核心)数量过多,导致线程过多。或者可能是“繁忙”的线程要求太多,导致所有这些额外的同步。顺便说一句,我非常感谢您的评论,只是想知道同步百分比何时开始表示问题。我不确定大约75%是否有问题。 - JoeGeeky
在我看来,75%似乎相当高,但是如果没有看到代码,很难说。可能是您的线程数过多,超出了可用核心数量,但也可能是其他原因。很难确定。我建议您阅读有关线程和线程同步的知识,因为只有深入理解这些内容,才能让您走得更远。 - Tony The Lion

1

线程同步有几种方式会影响性能:

  1. 实现同步的指令需要执行时间,特别是对于需要转换到内核模式的同步例程(如Thread.Sleep())而言。在最坏的情况下,一个单独的线程频繁调用同步例程会引入大量开销,却没有真正的好处。
  2. 当多个线程同时需要独占某个资源时,至少有一个线程会被阻塞等待。最糟糕的情况是,有一些中央资源每个人都需要频繁访问。在这种情况下,多线程代码很容易变成一种昂贵、缓慢和复杂的方式,只能让一个线程工作。

至于什么程度算太多:同步需要时间,但实际上并不做任何有用的工作。因此,从性能的角度来看,理想的同步量始终为零。因此,共享无状态架构和不可变数据结构的价值很高。两者都是帮助组织代码的技术,以消除或减少同步需求。

当然,世界并不完美,因此某种程度的同步通常是不可避免的。但即使如此,也应该尽可能使用最轻量级的结构来完成。例如,当一个方法在Interlocked中可用时,不要使用锁定语句。或通过设计线程向中央数据结构批量发送工作产品,而不是进行大量高频率的更新来减少它需要发生的频率。

互锁是否比锁更快,或者互锁的争用缓解与锁不同?如果我没记错,互锁会创建一个完整的栅栏(例如四个内存屏障)。我一直认为完整的栅栏和完整的锁一样昂贵。我的理解有误吗? - JoeGeeky
根据微软的“托管线程最佳实践”文章,它更快。我没有深入研究实现细节,但我的经验往往证实了这一点。 - Sean U

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