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 に答える
ロックはスレッドセーフを追加するための優れた一般的なアプローチですが、null以外の値が書き込まれるとすぐにフィールドが不変になる、1回限りの準不変性を含む多くのシナリオでは、Threading.Interlocked.CompareExchange
より良い場合があります。基本的に、そのメソッドはフィールドを読み取り、フィールドが指定された「比較」値と一致する場合にのみ、他の人がフィールドに触れる前に新しい値を書き込みます。いずれの場合も読み取られた値を返します。2つのスレッドが同時に試行しCompareExchange
、両方のスレッドがフィールドの現在の値を「比較」値として指定した場合、一方の操作は値を更新し、もう一方は更新せず、各操作は成功したかどうかを「認識」します。
CompareExchangeには2つの主な使用パターンがあります。1つ目は、変更可能なシングルトンオブジェクトを生成する場合に最も役立ちます。この場合、全員が同じインスタンスを表示することが重要です。
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
してロックを失うため、ロックできません。別の専用のロックオブジェクトが必要です。 - 変数への各アクセス、または少なくともすべての変更アクセスをロックする必要があります。それ以外の場合は、別の場所で変数を変更することで元に戻すことができるため、すべてのロックは無効になります。
- 理論的には、値を読み取るだけの場合はロックする必要はありませんが、ロックを再確認する必要があり、エラーが増える可能性があるため、ここでは行いません。
- 必ずしも読み取りアクセスをロックする必要はありませんが(前の2つのポイントを参照)、このプロパティへの読み取り/書き込みアクセスの並べ替えを防ぐために、どこかにメモリバリアを導入する必要がある場合があります。ルールが非常に複雑であるため、これがいつ関連するかはわかりません。これが、ロックが嫌いなもう1つの理由です。
全体として、コード設計を変更して、一度に1つのスレッドのみが特定の変数に書き込みアクセスできるようにし、スレッド間の必要なすべての通信を、同期されたデータ構造を介して明確に定義された通信チャネルに制限する方がはるかに簡単です。