这个简单的VB.Net类线程安全吗?如果没有,我该如何改进呢?

时间:2013-01-11 14:55:50

标签: vb.net thread-safety

Option Strict On

Public Class UtilityClass
  Private Shared _MyVar As String

  Public Shared ReadOnly Property MyVar() As String
    Get
      If String.IsNullOrEmpty(_MyVar) Then
        _MyVar = System.Guid.NewGuid.ToString()
      End If
      Return _MyVar
    End Get
  End Property

  Public Shared Sub SaveValue(ByVal newValue As String)
    _MyVar = newValue
  End Sub

End Class

2 个答案:

答案 0 :(得分:5)

虽然锁定是添加线程安全性的一种很好的通用方法,但在涉及一次写入准不可变性的许多场景中,只要向其写入非空值,字段就应该变为不可变,Threading.Interlocked.CompareExchange可能更好。本质上,该方法读取一个字段,并且 - 在其他人可以触摸它之前 - 当且仅当该字段与提供的“compare”值匹配时才写入新值;它返回在任何情况下读取的值。如果两个线程同时尝试CompareExchange,并且两个线程都将字段的当前值指定为“比较”值,则其中一个操作将更新该值,而另一个操作将不会,并且每个操作将“知道”是否为成功了。

CompareExchange有两种主要的使用模式。第一个对于生成可变单例对象最有用,其中每个人都看到相同的实例很重要。

If _thing is Nothing then
    Dim NewThing as New Thingie() ' Or construct it somehow
    Threading.Interlocked.CompareExchange(_thing, NewThing, Nothing)
End If

这种模式可能就是你所追求的。请注意,如果一个线程在另一个线程完成之间以及它执行CompareExchange的时间之间输入上述代码,则两个线程可能最终创建一个新的Thingie。如果发生这种情况,首先到达CompareExchange的线程将使其新实例存储在_thing中,而另一个线程将放弃其实例。在这种情况下,线程不关心它们是赢还是输; _thing将在其中包含一个新实例,并且所有线程都将在那里看到相同的实例。还要注意,因为在第一次读取之前没有内存屏障,理论上有可能在过去的某个时间检查过_thing的值的线程可能会继续将其视为Nothing,直到某些内容导致它更新其缓存,但如果发生这种情况,唯一的后果就是创建一个无用的Thingie新实例,当Interlocked.CompareExchange发现_thing已被写入时,该实例将被丢弃。

另一个主要用法模式对于更新对不可变对象的引用很有用,或者 - 稍作调整 - 更新某些值类型,如Integer或Long。

Dim NewThing, WasThing As Thingie
Do
    WasThing = _thing
    NewThing = WasThing.WithSomeChange();
Loop While Threading.Interlocked.CompareExchange(_thing, NewThing, WasThing) IsNot WasThing

在这种情况下,假设有一些方法,给定对Thingie的引用,可以廉价地生成一个以某种所需方式不同的新实例,可以以线程安全的方式对_thing执行任何此类操作。例如,给定String,可以轻松生成一个附加了一些字符的新String。如果有人希望以线程安全的方式将某些文本附加到字符串(如果一个线程尝试添加Fred而另一个尝试添加Joe,则最终结果将是追加FredJoeJoeFred,而不是FrJoeed),上面的代码会让每个帖子都读_thing,生成一个附加了文字的版本,然后尝试更新{ {1}}。如果某个其他线程在平均时间内更新_thing,则放弃构造的最后一个字符串,根据更新的_thing创建一个新字符串,然后重试。

请注意,虽然这种方法不一定比锁定方法更快,但它确实提供了一个优势:如果获取锁的线程陷入无限循环或者其他方式,则所有线程将永远被阻止访问锁定资源。相比之下,如果上面的_thing方法陷入无限循环,WithSomeChanges()的其他用户将不会受到影响。

答案 1 :(得分:3)

对于多线程代码,相关问题是:可以从多个线程修改状态吗?如果是这样,代码就不是线程安全的。

在你的代码中,情况就是这样:有几个地方变异_MyVar,因此代码不是线程安全的。使代码线程安全的最佳方法是几乎总是使其不可变:默认情况下,immutable状态只是线程安全。此外,不修改线程状态的代码比更改多线程代码更简单,通常更有效。

不幸的是,没有上下文就不可能看到你的代码是否(或如何)可以从几个线程变成不可变的。因此,我们需要采用缓慢,容易出错的锁定(请参阅另一个答案,了解错误是多么容易)并给出错误的安全感。

以下是我尝试使用锁来使代码更正。 应该工作(但要记住错误的安全感):

Public Class UtilityClass
  Private Shared _MyVar As String
  Private Shared ReadOnly _LockObj As New Object()

  Public Shared ReadOnly Property MyVar() As String
    Get
      SyncLock _LockObj
        If String.IsNullOrEmpty(_MyVar) Then
          _MyVar = System.Guid.NewGuid.ToString()
        End If
        Return _MyVar
      End SyncLock
    End Get
  End Property

  Public Shared Sub SaveValue(ByVal newValue As String)
    SyncLock _lockObj
      _MyVar = newValue
    End SyncLock
  End Sub

End Class

一些评论:

  • 我们无法锁定_MyVar,因为我们更改 _MyVar的引用,从而失去了我们的锁定。我们需要一个单独的专用锁定对象。
  • 我们需要锁定对变量的每次访问权限,或者至少锁定每次变异访问权限。否则所有锁定都是无效的,因为它可以通过在另一个地方更改变量来撤消。
  • 理论上我们不需要锁定,如果我们只读取该值 - 但是,这需要double-checked locking,这会带来更多错误的机会,所以我没有在这里完成。
  • 虽然我们不一定需要锁定读取访问(参见前两点),但我们可能仍需要在某处引入内存屏障,以防止对此属性的读写访问重新排序。我不知道什么时候变得相关,因为规则非常复杂,这是我不喜欢锁定的另一个原因。

总而言之,更改代码设计要容易得多,因此一次只能有一个线程对任何给定变量具有写访问权限,并且通过{{将线程之间的所有必要通信限制为明确定义的通信通道3}}