看起来你基本上是在问锁是如何工作的。锁如何在没有构建锁的情况下以原子方式维护内部状态?乍一看好像是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.SpinWait、Thread.Sleep(0)、Thread.Sleep(1) 和Thread.Yield。等待策略被过度简化,但它确实已经接近real life algorithm implemented in the BCL。我故意在上面的Enter 方法中保持代码简单,以便更容易发现关键位。这不是我通常会采用的实现方式,但我希望它确实能突出重点。
还要注意我上面的SimpleMonitor有很多问题。这里只是一些。
- 它不处理嵌套锁定。
- 它不像真正的
Monitor 类那样提供Wait 或Pulse 方法。他们真的很难做好。
1CLR 实际上会使用每个引用类型上存在的特殊内存块。这个内存块被称为“同步块”。 Monitor 将操纵这块内存中的位来获取和释放锁。此操作可能需要内核事件对象。您可以在Joe Duffy's blog 上阅读更多相关信息。