嵌入式锁是不是会针对竞争条件增加任何价值?
Posted
技术标签:
【中文标题】嵌入式锁是不是会针对竞争条件增加任何价值?【英文标题】:Do embedded locks add any value against race conditions?嵌入式锁是否会针对竞争条件增加任何价值? 【发布时间】:2021-04-26 07:40:21 【问题描述】:我最近在我的代码中遇到了一些竞争条件,我想知道二级/三级锁是否真的增加了任何价值。它们看起来非常多余,this SO post 似乎与我的思考过程一致,声明如下:
为了防止发生竞争条件,您通常会在共享数据周围加锁,以确保一次只有一个线程可以访问数据。这意味着这样的事情:
obtain lock for x
...release lock for x
给出这个从集合中删除空队列的简单示例:
Dictionary<Guid, Queue<int>> _queues = new Dictionary<Guid, Queue<int>>();
...
lock (_queues)
while (_queues.Any(a => a.Value.Count == 0))
Guid id = _queues.First(f => f.Value.Count == 0);
if (_queues.ContainsKey(id))
lock (_queues)
_queues.Remove(id);
第二把锁有什么价值吗?
【问题讨论】:
我会说不。如果一个线程进入第一个锁,其他线程将无法进入该代码块。所以这不会帮助你解决你的比赛条件。您可以尝试将其更改为ConcurrentQueue
。
【参考方案1】:
在您发布的代码中,不,第二个锁不会添加任何值,因为如果不先执行 while 循环就无法访问该代码,此时互斥锁已经被锁定。
可能需要注意的是,将它放在那里并没有什么坏处,就像C# locks are reentrant。
第二个锁确实增加了价值的代码中不清楚是否总是会获得第一个锁。例如:
void DoWork1()
lock(_queues)
//do stuff...
if(condition)
DoWork2();
void DoWork2()
lock(_queues)
//do stuff...
【讨论】:
【参考方案2】:第二把锁有什么价值吗?
答案显然是否定的,你的直觉是正确的,但让我们合理化原因。
lock
的工作方式是调用Monitor.Enter/Exit
。
当您使用lock
时,CLR 标记了 object 的 header(或其到同步表的链接)atomically 带有一个 thread Id 和一个 counter。
如果另一个 thread 尝试使用现有的 Thread ID
锁定 object-
它将执行一个小的自旋等待(薄锁)以有效地等待释放而无需上下文切换
如果做不到这一点,它将采取更积极的方法将 lock 提升为具有 操作系统 的 事件 并等待该句柄(脂肪锁)。
每次同一个线程在同一个object上调用同一个lock
,它(本质上)只是增加counter header 或 (sync block) 中的 object 并继续有增无减,直到它退出锁 (Monitor.Exit
),此时它会递减计数器。以此类推,直到计数器为零。
那么... 单线程 代码的同一主体中的同一对象 上的两个嵌套锁 有什么作用吗?答案是否定的,除了增加锁定计数器(以少量成本)。
所以你可能会问这个问题,所有这些counter'ing有什么用?嗯,它实际上适用于更复杂的可重入代码场景,或者你有可能将代码重定向到同一对象上的锁的分支。 .. 在这种情况下,同一个 thread 不会在同一个 lock 上阻塞,从而否定一个非常真实的 deadlock em> 情况。
注意:CLR 实现和操作系统特定的内部锁定如何工作有一些组件,但是 ECMA 规范保证了一定的这些同步原语在行为、重入、结构化/发出的代码和 jit 重新排序方面需要如何工作的一致实现。
其他资源
有关所有血腥细节,您可以在此处查看我的答案
Why Do Locks Require Instances In C#?
【讨论】:
【参考方案3】:来自language specification:
在持有互斥锁的同时,在同一个执行线程中执行的代码也可以获得和释放锁。但是,在锁被释放之前,在其他线程中执行的代码被阻止获取锁
所以内部锁是允许的,但没有实际作用,因为它已经在临界区了。
为避免死锁,我建议对您在持有锁时调用的代码非常严格。使用框架集合可能很好,但如果你调用一些任意方法,它可能会尝试获取另一个锁。这对事件来说很容易做到,你引发事件,它会做一些其他的事情,然后 20 次调用之后它会尝试锁定。使用 Task.Result
或 Task.Wait
是导致死锁的其他潜在原因。
有时可以使用并发集合来代替锁。另一种方法是使用exclusive task scheduler 来确保某些资源永远不会被多个线程使用。
【讨论】:
以上是关于嵌入式锁是不是会针对竞争条件增加任何价值?的主要内容,如果未能解决你的问题,请参考以下文章