2

如果我有这样的声明怎么会:

private int sharedValue = 0;

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

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

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

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

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

4

2 回答 2

2

看起来你基本上是在问锁是如何工作的。锁如何在没有构建锁的情况下以原子方式维护内部状态?乍一看似乎是先有鸡还是先有蛋的问题,不是吗?

由于比较和交换(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.SpinWait,Thread.Sleep(0)和在等待获取锁时使用的Thread.Sleep(1)Thread.Yield等待策略被过度简化了,但它确实接近了BCL 中实现的现实生活中的算法。我故意在Enter上面的方法中保持代码简单,以便更容易发现关键位。这不是我通常会如何实施的方式,但我希望它确实能把重点放在家里。

另请注意,我的SimpleMonitor上面有很多问题。这里只是少数。

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

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

于 2013-10-28T02:10:47.480 回答
1

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

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

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

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

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

于 2013-10-28T01:27:29.647 回答