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 回答
虽然锁定是增加线程安全性的一种很好的通用方法,但在许多涉及一次性写入准不变性的场景中,一旦将非空值写入字段就应该变得不可变,这Threading.Interlocked.CompareExchange
可能会更好。本质上,该方法读取一个字段,并且在其他人可以触摸它之前写入一个新值,当且仅当该字段与提供的“比较”值匹配时;它返回在任何情况下读取的值。如果两个线程同时尝试 a 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
,则最终结果将是追加FredJoe
或JoeFred
,而不是类似的东西FrJoeed
),上面代码将读取每个线程_thing
,生成附加文本的版本,然后尝试更新_thing
. 如果其他一些线程更新_thing
同时,放弃上一个构造的字符串,根据更新的字符串创建一个新字符串_thing
,然后重试。
请注意,虽然这种方法不一定比锁定方法快,但它确实提供了一个优势:如果获取锁的线程陷入无限循环或以其他方式被阻塞,所有线程将永远被阻止访问锁定的资源。相比之下,如果WithSomeChanges()
上面的方法陷入死循环,其他用户_thing
不会受到影响。
对于多线程代码,相关问题是:可以从多个线程修改状态吗?如果是这样,则代码不是线程安全的。
在您的代码中,情况就是这样:有几个地方发生了变异_MyVar
,因此代码不是线程安全的。使代码线程安全的最佳方法几乎总是使其不可变:不可变状态在默认情况下只是线程安全的。此外,不跨线程修改状态的代码比修改多线程代码更简单且通常更有效。
不幸的是,如果没有上下文,就不可能看到您的代码是否(或如何)在多个线程中是不可变的。因此,我们需要使用速度慢、容易出错的锁(请参阅另一个答案,了解它是多么容易出错)并给人一种虚假的安全感。
以下是我尝试使用锁使代码正确。它应该可以工作(但请记住错误的安全感):
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
,从而失去了锁定。我们需要一个单独的专用锁定对象。 - 我们需要锁定对变量的每次访问,或者至少锁定每次变异访问。否则所有的锁定都是徒劳的,因为它可以通过在另一个地方更改变量来撤消。
- 从理论上讲,如果我们只读取值,我们不需要锁定 - 但是,这将需要双重检查锁定,这会引入更多错误的机会,所以我在这里没有这样做。
- 尽管我们不一定需要锁定读取访问(参见前两点),但我们可能仍需要在某处引入内存屏障以防止对该属性的读写访问重新排序。我不知道这什么时候会变得相关,因为规则非常复杂,这也是我不喜欢锁的另一个原因。
总而言之,更改代码设计以便一次不超过一个线程对任何给定变量具有写访问权,并通过同步数据结构将线程之间所有必要的通信限制为定义良好的通信通道,要容易得多。