看起来你基本上是在问锁是如何工作的。锁如何在没有构建锁的情况下以原子方式维护内部状态?乍一看似乎是先有鸡还是先有蛋的问题,不是吗?
由于比较和交换(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
上面有很多问题。这里只是少数。
- 它不处理嵌套锁定。
- 它不提供
Wait
或Pulse
像真正的Monitor
类那样的方法。他们真的很难做对。
1 CLR 实际上会使用每个引用类型上存在的特殊内存块。这个内存块被称为“同步块”。将Monitor
操纵这块内存中的位来获取和释放锁。此操作可能需要内核事件对象。您可以在Joe Duffy 的博客上阅读更多相关信息。