嵌入式锁是否为竞争条件增加了任何价值?



我最近在代码中遇到了一些竞争条件,我想知道二级/三级锁是否真的增加了任何价值。它们看起来非常多余,这篇文章似乎与我的想法一致,它说:

为了防止发生竞争条件,您通常会在共享数据周围放置一个锁,以确保一次只有一个线程可以访问数据。它的意思是:

obtain lock for xrelease 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);
}
}

第二个锁是否提供任何值?

在您发布的代码中,不,第二个锁不会增加任何值,因为如果不首先执行while循环,就不可能到达该代码,此时互斥锁已经被锁定。

需要注意的是,把它放在这里并没有什么坏处,因为c#锁是可重入的。

第二个锁确实增加了价值的地方是在代码中不清楚是否总是会获得第一个锁的地方。例如:

void DoWork1()
{
lock(_queues)
{
//do stuff...
if(condition)
DoWork2();
}
}
void DoWork2()
{
lock(_queues)
{
//do stuff...
}
}

第二个锁是否提供任何值?

答案显然是否定的,你的直觉是正确的,但是让我们来解释一下为什么。

lock的工作方式是调用Monitor.Enter/Exit

  • 当您取lock时,CLR标记对象(或其到同步表的链接)自动线程Id计数器

  • 如果另一个线程试图用已存在的线程ID锁定对象

    1. 它将执行一个小的旋转等待(瘦锁)来有效地等待释放,而不需要上下文切换
    2. 如果做不到这一点,它将采取更积极的方法将提升到事件操作系统并等待该句柄(胖锁)。
  • 每次相同的线程在相同的对象上调用相同的lock时,它(本质上)只是在或(同步块)中增加对象计数器,并继续不减,直到退出锁(Monitor.Exit),此时它减少计数器。依此类推,直到计数器为零。

所以…两个嵌套的锁在同一个对象上,在同一个单线程代码体中实现了什么吗?答案是否定的,除了增加锁计数器(花费很少)。

所以你可能会问,这些计数器有什么用?嗯,它实际上适用于更复杂的可重入代码场景,或者你有分支,可能会将代码重定向到相同对象上的…在这种情况下,相同的线程不会在相同的上阻塞,从而否定了非常真实的死锁

情况。<子>注意:内部锁的工作方式有一些组件是CLR实现和特定于操作系统的,但是ECMA规范保证了这些同步原语在行为、重入口、结构/发出代码和jit重排序方面的工作方式是一致的。


额外资源

关于所有血腥的细节,你可以在这里看到我的答案

为什么锁需要实例?

来自语言规范:

当持有互斥锁时,在同一执行线程中执行的代码也可以获取和释放该锁。但是,在其他线程中执行的代码被阻止获取锁,直到锁被释放

所以内锁是允许的,但是没有实际效果,因为它已经在临界区了。

为了避免死锁,我建议在持有锁时对调用的代码进行严格限制。使用框架集合可能没问题,但如果调用任意方法,它可能会尝试获取另一个锁。这对于事件来说是很容易做到的,你引发事件,它做一些其他的事情,20次调用之后它会尝试获取一个锁。使用Task.ResultTask.Wait是导致死锁的其他潜在原因。

有时可以使用并发集合来代替锁。另一种方法是使用独占任务调度器来确保某些资源永远不会被多个线程使用。

相关内容