【问题标题】:Can the same object lock be checked at the same time by concurrent threads?并发线程可以同时检查同一个对象锁吗?
【发布时间】:2013-10-27 23:26:28
【问题描述】:

如果我有这样的说法怎么办:

private int sharedValue = 0;

public void SomeMethodOne()
{
   lock(this){sharedValue++;}
}

public void SomeMethodTwo()
{
   lock(this){sharedValue--;}
}

因此,一个线程要进入锁,它必须首先检查是否有另一个线程在对其进行操作。如果不是,它可以进入并且必须向内存中写入一些东西,这肯定不是原子的,因为它需要读取和写入。

那么为什么一个线程不可能读取锁,而另一个线程正在向它写入所有权呢?

为了简化为什么两个线程不能同时进入锁?

【问题讨论】:

  • 在此处查看另一个类似问题:stackoverflow.com/questions/14758088/…
  • 它们是原子的。具体来说,它们是同步块索引的原子更新。它只是一个整数。
  • 这是锁定点 - 这样您就可以编写可供并发线程安全使用的代码。

标签: c# multithreading concurrency locking


【解决方案1】:

看起来你基本上是在问锁是如何工作的。锁如何在没有构建锁的情况下以原子方式维护内部状态?乍一看好像是chicken and egg problem,不是吗?

所有的奇迹都是因为compare-and-swap (CAS) 操作而发生的。 CAS 操作是一个硬件级指令,它做了两件重要的事情。

  • 它会生成一个内存屏障,从而限制指令重新排序。
  • 它将内存地址的内容与另一个值进行比较,如果它们相等,则将原始值替换为新值。它以原子方式完成所有这些工作。

在最基本的层面上,这就是技巧的完成方式。并不是所有其他线程都被阻止读取,而另一个线程正在写入。这是完全错误的思考方式。实际发生的是所有线程同时充当作家。该策略比悲观更乐观。每个线程都试图通过执行这种称为 CAS 的特殊写入来获取锁。您实际上可以通过 Interlocked.CompareExchange (ICX) 方法访问 .NET 中的 CAS 操作。每个同步原语都可以从这个单一操作构建。

如果我要完全在 C# 中从头开始编写类似 Monitor 的类(这就是 lock 关键字在幕后使用的),我可以使用 Interlocked.CompareExchange 方法来完成。这是一个过于简化的实现。请记住,这肯定不是 .NET Framework 是如何做到的。1我提供以下代码的原因是为了向您展示它如何 用纯 C# 代码完成,无需在幕后使用 CLR 魔法,因为它可能会让您思考 Microsoft 如何实现它。

public class SimpleMonitor
{
    private int m_LockState = 0;

    public void Enter()
    {
        int iterations = 0;
        while (!TryEnter())
        {
            if (iterations < 10) Thread.SpinWait(4 << iterations);
            else if (iterations % 20 == 0) Thread.Sleep(1);
            else if (iterations % 5 == 0) Thread.Sleep(0);
            else Thread.Yield();
            iterations++;
        }
    }

    public void Exit()
    {
        if (!TryExit())
        {
            throw new SynchronizationLockException();
        }
    }

    public bool TryEnter()
    {
        return Interlocked.CompareExchange(ref m_LockState, 1, 0) == 0;
    }

    public bool TryExit()
    {
        return Interlocked.CompareExchange(ref m_LockState, 0, 1) == 1;
    }
}

这个实现展示了几个重要的事情。

  • 它展示了如何使用 ICX 操作以原子方式读取和写入锁定状态。
  • 它显示了等待是如何发生的。

请注意我在等待获取锁时如何使用Thread.SpinWaitThread.Sleep(0)Thread.Sleep(1)Thread.Yield。等待策略被过度简化,但它确实已经接近real life algorithm implemented in the BCL。我故意在上面的Enter 方法中保持代码简单,以便更容易发现关键位。这不是我通常会采用的实现方式,但我希望它确实能突出重点。

还要注意我上面的SimpleMonitor有很多问题。这里只是一些。

  • 它不处理嵌套锁定。
  • 它不像真正的Monitor 类那样提供WaitPulse 方法。他们真的很难做好。

1CLR 实际上会使用每个引用类型上存在的特殊内存块。这个内存块被称为“同步块”。 Monitor 将操纵这块内存中的位来获取和释放锁。此操作可能需要内核事件对象。您可以在Joe Duffy's blog 上阅读更多相关信息。

【讨论】:

  • 嗨。感谢您的回复。我正在阅读 Duffy 的书(Windows 上的并发编程),它讨论了 Dekker 和 Dijkstra 的算法,一直到你上面提到的内容。
  • @user1743962:这是一本好书。请务必阅读从 pg 开始的旋转等待部分。 767. 这是我在回答中设计等待策略时使用的另一个参考。
【解决方案2】:

C#中的lock用于创建Monitor对象,实际用于锁定。

您可以在此处阅读有关Monitor 的更多信息:http://msdn.microsoft.com/en-us/library/system.threading.monitor.aspxMonitorEnter方法保证了此时只有一个线程可以进入临界区:

获取对象的锁。这个动作也标志着临界区的开始。没有其他线程可以进入临界区,除非它使用不同的锁定对象执行临界区中的指令。

顺便说一句,您应该避免锁定this (lock(this))。您应该在类(静态或非静态)上使用私有变量来保护临界区。您可以在上面提供的同一链接中阅读更多内容,但原因是:

选择要同步的对象时,您应该只锁定私有或内部对象。锁定外部对象可能会导致死锁,因为不相关的代码可能会出于不同目的选择相同的对象进行锁定。

【讨论】:

    猜你喜欢
    • 2023-03-19
    • 1970-01-01
    • 2015-02-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多