【问题标题】:Why lock is needed to implement a readonly int property?为什么需要锁来实现只读的 int 属性?
【发布时间】:2010-10-04 08:33:03
【问题描述】:

我是线程新手,在blog 中遇到了一个自定义线程池实现示例。我只是粘贴代码的必要部分:

Public Class ThreadPool

    Private CountLock As New Object
    Private _Count As Integer

    Public ReadOnly Property ThreadCount() As Integer
      Get
         SyncLock CountLock
           Return _Count
         End SyncLock
      End Get
    End Property

    Public Sub Open()
      Interlocked.Increment(_Count)
    End Sub

    Public Sub Close()
      Interlocked.Decrement(_Count)
      ....
    End Sub

EndClass

我的问题是,为什么我们需要一个锁来实现只读的 ThreadCount 属性?

【问题讨论】:

  • 链接中的代码甚至不像线程池。
  • 我想最后我会把这个页面的链接发送给博客的作者:)

标签: .net multithreading locking


【解决方案1】:

这段代码应该使用Interlocked.CompareExchange 来访问属性getter 中的值。将 param3 (comparand) 设置为你知道在变量中看不到的东西,比如Int32.MinValue,然后函数只返回_count 的当前值。

如果Interlocked 操作用于对变量的所有访问,则锁是多余的,因为通过Interlocked 类方法的所有访问都是原子的。

【讨论】:

  • 我想我在学习锁的时候漏掉了一些东西。你能解释一下为什么如果他没有使用互锁,而只是在打开/关闭中使用锁定,为什么需要锁定?
  • @davsan - 互锁类使用特定于平台的处理器指令来保证访问基本类型变量(如Int32Int64)或本机指针时的原子行为。如果您可以确保对_count 的所有访问都是通过Interlocked,这将是在_count 上处理线程安全的最有效方法。任何更高级别的锁都会更昂贵。否则,请摆脱 Interlocked 的用法并在任何地方使用 lock(_count) - 混搭没有意义。
  • @Steve - 好的,我明白你的解释了。我想我想念的是:afaik, lock (CountLock) { return _count;} 限制一次只能有一个线程执行 {return _count} 部分,所以如果多个线程在同一部分执行同一部分会有什么缺点同时?我想我没有意识到阅读安全问题?
  • @davsan - 只要 _count 不在其他任何地方使用,这可能会起作用。但是,我不确定将互锁操作(在机器级别锁定)与非互锁操作(例如,使用托管代码 lock() 保护)混合起来有多可靠。如果您碰巧进入托管代码 lock(),而另一个线程正在通过 Interlocked.DecrementInterlocked.Increment 更新底层内存,我不确定是否可以保证原子读取。除非您确定,否则我不会混合使用这些数据保护类型。
  • @davsan - 另请参阅我关于此主题的问题:stackoverflow.com/questions/3855904/…
【解决方案2】:

我不知道为什么作者选择在类的一个部分使用锁,而在其他部分使用无锁技术。但是,我可以假设作者这样做是为了在读取Interger 时创建一个显式内存屏障。 VB 不包含等效于 C# 的 volatile 关键字,因此只剩下 4 种其他常用方法来确保读取安全。我已经按照我为这个特定场景选择的顺序列出了这些。

  • Interlocked.CompareExchange
  • Thread.VolatileRead
  • Thread.MemoryBarrier
  • 同步锁定

需要内存屏障来防止 VB 或 JIT 编译器移动指令。在没有内存屏障的情况下,最可能的优化是将读取提升到循环之外。考虑一下ThreadCount 属性的这种实际用法。

Sub LoggingThread()
  Do While True
    Trace.WriteLine(ThreadPool.ThreadCount)
  Loop
End Sub

在此示例中,CLR 可能会内联 ThreadCount,然后可能会“提升”对 _Count 的读取,并在循环开始之前将其缓存在 CPU 寄存器中。效果是始终显示相同的值。1

1实际上,Trace.WriteLine 调用本身会生成一个内存屏障,这会导致代码意外安全。该示例旨在简单说明可能发生的情况。

【讨论】:

    【解决方案3】:

    锁将强制设置内存屏障,因此如果最后写入的值是由不同的 CPU 写入的,则不会读取 CPU 缓存中的陈旧值。没有锁定的Thread.VolatileRead() 也可以这样做。

    【讨论】:

      【解决方案4】:

      这没有任何意义,因为您修改属性的位置没有锁。也许代码之前没有使用互锁操作,甚至在打开/关闭时也使用了 SyncLock?在这种情况下,读访问也确实需要 SyncLock。

      【讨论】:

      • 你确定这没有意义吗?添加锁会添加一个内存屏障,这将阻止指令重新排序,可能会有所作为。
      • 防止对像 ThreadCount 这样的属性进行指令排序?我不这么认为——它纯粹是一个信息/调试属性。任何真正的逻辑都可以用 _Count 来完成。
      • @Lasse 单独阅读 int 在 .NET 中是原子的。如果调用者也调用其他成员并且有足够的内联进行,那么重新排序只会是一个问题:它可能是但会暗示紧密耦合,这可能意味着锁定应该在调用者中。
      • 可能是,我只想指出,这个问题可能不像所示的那样明确。我肯定不知道答案。
      • @Richard,重新排序不会有问题,但 CPU 缓存可以。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-11
      • 1970-01-01
      • 2011-08-11
      • 2019-08-07
      相关资源
      最近更新 更多