1

我看了杰弗里·里希特的视频(点击查看确切的线路),他说

在 Event-wait-handles 或 Semaphore 等上使用 Monitor.Enter 和 Monitor.Lock 总是更好,因为它们(monitor.X)使用内核对象,但只有在存在争用时才会使用它们。如果没有争用,他们就不会使用这些对象。

我可能在这里遗漏了一些东西,但是当我这样做时:

lock(myObj)
{
 ...
}

认为可能有不止一个线程想要进入临界区。
那么,根据上面的信息,如果没有争用,锁就不会被使用?(如果另一个线程即将进入 1 ms 后怎么办?)

4

3 回答 3

3

是的,Monitor 类在 CLR 中得到了很好的优化。如果没有其他线程已经拥有监视器,这是非常便宜的。您甚至不需要为额外的存储付费,锁定状态存储在 Object 中的一个字段中,每个对象都已经可用,它是对象头的一部分。

Monitor.Enter() 方法通过首先检查锁是否已由同一线程拥有来避免输入操作系统代码。这使它可重入,如果是这种情况,它只会增加锁计数。接下来,它尝试使用相当于 Interlocked.CompareExchange() 的等价物来获取锁,这是任何处理器上非常便宜的原语。它的 x86 版本值得注意的是实际上根本没有使用总线锁,您可以在这个答案中看到它的代码。

如果这不起作用,那么操作系统需要参与进来,因为现在重要的是它可以进行线程上下文切换以在释放锁时唤醒线程。Windows 非常倾向于选择一个正在等待操作系统同步对象的线程,以确保它尽快重新开始运行。底层对象是一个简单的事件,一个非常便宜的 OS 对象。它还照顾公平,等待线程被放入队列并以先进先出的顺序释放。我在这个答案中记录了底层 CLR 代码。

于 2013-03-16T12:43:47.257 回答
2

lock语句只是使用Monitor.EnterMonitor.Exit作为其实现的语法糖。

Monitor函数本身是使用条件变量实现的。它们的实现意味着它们不需要分配内核对象,除非确实存在对锁的争用。当这种情况发生时,他们必须继续分配一个内核对象。

即使存在锁争用,它们也不会立即分配内核对象。相反,他们“旋转”(只是在一个紧密的循环中坐了一会儿),希望锁变得自由。只有当它没有变得空闲时,它才会继续分配/使用内核对象。

请注意,一些新的同步类,例如ManualResetEventSlim也采用这种方法。(通常,任何末尾带有“Slim”的同步类都会这样做。)

请参阅此线程

直接回答您的问题lock:是的,如果没有争用,或者争用只持续很短的时间,则不会为了使用内核对象而转换到内核模式。只有当争用持续超过很短的时间时,才会转换到内核模式。

于 2013-03-16T10:58:10.093 回答
2

那么,根据上面的信息,如果没有争用,锁就不会被使用?(如果另一个线程即将进入 1 ms 后怎么办?)

正确的。然后是争用,另一个线程将不得不进入内核。此外,拥有锁的线程在解锁时也必须进入内核。

操作看起来像这样:

锁:

  1. 尝试以原子方式将用户空间锁变量从解锁设置为锁定。如果我们这样做,停下来,我们就完成了。

  2. 增加用户空间争用计数。

  3. 将内核空间锁设置为锁定。

  4. 尝试以原子方式将用户空间锁变量从解锁设置为锁定。如果我们这样做,减少用户空间争用计数并停止,我们就完成了。

  5. 块在内核上的内核锁。

  6. 转到第 3 步。

开锁:

  1. 以原子方式将用户空间锁定变量从锁定设置为解锁。

  2. 如果用户空间争用计数为零,停止,我们就完成了。

  3. 将内核锁设置为解锁。

请注意,如果没有争用,锁定操作仅涉及步骤 1,而解锁操作仅涉及步骤 1 和 2,所有这些都发生在用户空间中。

于 2013-03-16T11:37:20.417 回答