嵌入式锁是不是会针对竞争条件增加任何价值?

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 标记了 objectheader(或其到同步表的链接)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.ResultTask.Wait 是导致死锁的其他潜在原因。

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

【讨论】:

以上是关于嵌入式锁是不是会针对竞争条件增加任何价值?的主要内容,如果未能解决你的问题,请参考以下文章

在嵌入式系统的上下文中,啥是外围锁定?

在事件驱动的嵌入式系统中使用事件队列避免竞争条件

通知条件变量并解锁关联互斥锁后的“数据竞争”(不是真的)

嵌入式开发之hi3519---进程线程间的同步和互斥,条件变量信号了互斥锁等

以下无锁代码是不是表现出竞争条件?

为啥“删除”这个无锁堆栈类中的节点会导致竞争条件?