嵌入式锁是否增加了对抗竞争条件的价值?

Do embedded locks add any value against race conditions?

我最近在我的代码中遇到了一些竞争条件,我想知道 secondary/tertiary 锁是否真的增加了任何价值。它们看起来非常多余,this SO post 似乎与我的思考过程一致:

In order to prevent race conditions from occurring, you would typically put a lock around the shared data to ensure only one thread can access the data at a time. This would mean something like this:

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);
    }
}

第二把锁是否提供任何价值?

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

可能重要的是要注意,将它放在那里并没有什么坏处,因为 C# locks are reentrant

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

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

void DoWork2()
{
    lock(_queues)
    {
        //do stuff...
    }
}

来自language specification

While a mutual-exclusion lock is held, code executing in the same execution thread can also obtain and release the lock. However, code executing in other threads is blocked from obtaining the lock until the lock is released

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

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

有时可以使用并发集合来代替锁。另一种方法是使用 exclusive task scheduler 来确保某些资源永远不会被多个线程使用。

Does the second lock provide any value?

答案显然是否定的,你的直觉是正确的,但让我们合理化原因。

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

  • 当你使用 lock 时,CLR 标记 header ]object(或其 link 到同步 table)atomically with a thread Id and一个 计数器.

  • 如果另一个线程尝试锁定 对象一个现有的 线程 ID

    1. 它将执行一个小的 自旋等待(瘦锁)以高效地等待释放而无需 上下文切换
    2. 否则,它将采取更积极的方法将 升级为具有 操作的 事件 system 并等待该句柄(胖锁)。
  • 每次同一个线程在同一个对象上调用同一个lock,它(本质上)只是增加 headerobjectcounter 或( sync block) 并继续有增无减,直到退出锁定 (Monitor.Exit),此时它递减 counter。依此类推,直到计数器为零。

所以...在单线程[的同一个主体对象上做两个嵌套的 =87=]代码实现了什么?嗯,答案是否定的,除了增加 锁定计数器 (以很小的成本)。

所以您可能会问,所有这些反击有什么用?好吧,它实际上是针对更复杂的 可重入 代码场景,或者您有分支可能会将代码重定向到 lock 相同 object... 在那种情况下,相同的 thread 不会 block 相同的 lock ,进而否定了一个非常真实的 死锁 情况。

注意CLR 实现 内部锁定的工作原理有一些组成部分和操作系统特定的,但是 ECMA 规范保证了这些 同步原语 需要如何在行为、重入、structural/emitted 代码和 jit 重新排序方面工作的某种一致的实现。


其他资源

对于所有血淋淋的细节,您可以在此处查看我的回答