什么是线程竞争?

160

有人能简单地解释一下什么是线程争用吗?

我查了谷歌,但似乎找不到简单明了的解释。


13
请写下您对此的含糊想法,这样我们就可以看出您可能存在哪些偏差,或者您的理解是否正确。请将其翻译成中文,不改变原意,不加解释,不返回其他内容。 - James Black
10个回答

249

很多答案似乎都集中在锁竞争上,但锁并不是唯一可能发生争用的资源。争用指当两个线程试图以某种方式访问同一资源或相关资源时,至少有一个争用线程的运行速度比如果其他线程未运行则更慢。

最明显的争用例子是在锁上。如果线程A获取了锁,而线程B想要获取相同的锁,那么线程B将必须等待直到线程A释放锁。

现在,这个问题具体取决于平台,但即使它从来没有等待另一个线程释放锁,该线程也可能会经历减速!这是因为锁保护某些数据,而数据本身也经常会被争用。

例如,考虑一个线程获取锁、修改对象,然后释放锁并执行其他操作。如果有两个线程这样做,即使它们从不为锁而战,线程也可能比只有一个线程运行时慢得多。

为什么?假设每个线程在现代x86 CPU的自己核心上运行且核心不共享L2缓存。对于只有一个线程,对象大部分时间可能都在L2缓存中。如果两个线程都在运行,每当一个线程修改对象时,另一个线程将发现数据不在其L2缓存中,因为另一个CPU使缓存行无效。例如,在Pentium D上,这将导致代码运行在FSB速度,比L2缓存速度慢得多。

由于即使锁本身没有被争用也可能发生争用,因此在没有锁的情况下,争用也可能会发生。例如,假设你的CPU支持32位变量的原子递增。如果一个线程不断地递增和递减一个变量,该变量将大部分时间保留在缓存中。如果两个线程这样做,它们的缓存将争夺拥有该变量的内存,并且许多访问将变慢,因为缓存一致性协议操作来确保每个核心拥有缓存行的所有权。

具有讽刺意味的是,锁通常会减少竞争。为什么呢?因为没有锁,两个线程可能会在同一个对象或集合上操作,并引起很多竞争(例如,无锁队列)。锁将倾向于取消争用的线程,允许非争用的线程运行。如果线程A持有锁,而线程B也想要相同的锁,实现可以运行线程C。如果线程C不需要该锁,则可以暂时避免线程A和B之间的未来竞争(当然,这假设存在其他线程可以运行。如果系统整体只能通过运行争用线程来取得实用进展,则无济于事)。


12
为了明确一点,两个核心争夺的两个变量甚至不需要是同一个变量,只要它们存储在同一个缓存行中就会引起争用。填充结构和/或将结构对齐到内存可以帮助避免这种争用形式。 - Rob_before_edits
1
@David,请帮忙更详细地解释你回答中的最后一段。 - lowLatency
4
请提出一个关于此事的问题。 - David Schwartz
@Pacerier 主要使用C ++。 - David Schwartz
1
我不是很确定。我可能简化了情景,实际情况是有几个其他线程处理telnet连接,其中一个线程通过telnet处理一些guile输入。因此,在我之前的解释中,T2是一个guile线程,等待评估其输入(创建Hypergraph查询),而T1是一个C++线程(在Hypergraph对象上执行读取查询)。还有一些额外的短暂guile线程从C++代码中创建。 - Misgevolution
显示剩余3条评论

111

线程争用本质上是一个条件,其中一个线程正在等待由另一个线程持有的锁/对象。因此,这个等待线程在另一个线程解锁该特定对象之前不能使用该对象。


64
这个答案是不完整的(和大多数其他答案一样)。虽然锁是可能存在争用的一种类型,但它远远不是唯一可能存在争用的东西。非锁定资源也可能存在争用。(例如,如果两个线程不断原子地递增相同的整数,它们可能会由于缓存弹球而经历争用。没有涉及锁。) - David Schwartz
在像CPython中存在全局解释器锁(GIL)的情况下,其中线程必须始终获取GIL,因此在同一进程中运行的多个线程默认情况下会争用资源。 - Asclepius
1
我认为您已经用死锁的术语来解释它,但是它与死锁非常不同。 - Harshit Gupta

26

来自这里

当线程等待一个资源无法立即使用时,就会发生争用(contention);这会减缓代码的执行速度,但随着时间的推移可能会消失。

当一个线程等待第二个线程锁定的资源,而第二个线程又等待第一个线程锁定的资源时,就会发生死锁(deadlock)。多个线程可能会涉及到死锁。死锁永远不会自动解决,通常会导致整个应用程序或遇到死锁的部分停止运行。


这也解释了线程争用和死锁之间的区别。 - Sankalp

4
我认为OP应该对问题的背景进行一些澄清 - 我能想到两个答案(虽然我相信这个列表还会有其他的):
1. 如果您在谈论线程争用的“概念”以及它如何在应用程序中出现,我推荐参考@DavidSchwartz上面详细的回答。
2. 还有“.NET CLR锁和线程:总争用次数”性能计数器。从此计数器的PerfMon描述中获取,定义为:
此计数器显示CLR中的线程尝试未成功地获得托管锁的总次数。可以通过许多方式获取托管锁;通过C#中的“lock”语句或调用System.Monitor.Enter或使用方法ImplOptions.Synchronized自定义属性。
...我相信其他操作系统和应用程序框架也会有类似的内容。

3
对我而言,争用(Contention)是指两个或更多线程竞争共享资源,这些资源可能是锁、计数器等。竞争意味着“谁先获得它”。线程越多,争用就越多。对资源的访问越频繁,争用就越多。

2

您有两个线程,分别是线程A和线程B,同时还有对象C。

线程A当前正在访问对象C,并已在该对象上放置了锁。 线程B需要访问对象C,但在A释放对象C的锁之前无法这样做。


1

另一个词可能是并发。它只是两个或更多线程尝试使用相同资源的想法。


0

线程争用也受I/O操作的影响。例如,当一个线程等待文件读取时,可以看作是一种争用。使用I/O完成端口作为解决方案。


0
想像一下以下情景。你正在准备明天的期末考试,感到有点饿。于是,你给你的弟弟10元钱,让他为你买披萨。在这种情况下,你是主线程,你的弟弟是子线程。一旦你下了订单,你和你的弟弟就同时进行自己的工作(即学习和买披萨)。现在,我们有两种情况需要考虑。首先,你的弟弟带回了你的披萨并终止了操作,而此时你正在学习。在这种情况下,你可以停止学习,享受披萨。其次,你完成了学习并且睡觉(也就是你今天分配的工作——学习明天的期末考试——已经完成),但在披萨可用之前。当然,你不能睡觉;否则,你就没有机会吃披萨了。你要做的就是等待你的弟弟把披萨拿回来。
正如例子中所示,这两种情况说明了竞争关系的含义。

0
当一个线程试图获取一个已经被其他线程占用的对象的锁时,就会发生锁争用。在该对象被释放之前,该线程将被阻塞(换句话说,它处于等待状态)。在某些情况下,这可能导致所谓的串行执行,从而对应用程序产生负面影响。 来自dotTrace documentation

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