スレッドセーフコードの記述方法を理解したいと思います。
たとえば、ゲームにこのコードがあります:
_bool _done = false;
Thread _thread;
// main game update loop
Update()
{
// if computation done handle it then start again
if(_done)
{
// .. handle it ...
_done = false;
_thread = new Thread(Work);
_thread.Start();
}
}
void Work()
{
// ... massive computation
_done = true;
}
_
私がそれを正しく理解していれば、メインゲームスレッドと私の__thread
_が__done
_の独自のキャッシュバージョンを持つことができ、あるスレッドが別のスレッドで__done
_が変更されたことを決して見ないことがありますか?
そして、もしそうなら、それを解決する方法は?
volatile
キーワードのみを適用して解決することは可能ですか?.
または、Interlocked
やExchange
などのRead
のメソッドを使用して値を読み書きすることはできますか?
__done
_読み取りおよび書き込み操作をlock (_someObject)
で囲む場合、Interlocked
またはキャッシュを防ぐために何かを使用する必要がありますか?
編集1
_done
_をvolatile
として定義し、Update
メソッドを複数のスレッドから呼び出した場合。 __done
_をfalseに割り当てる前に、2つのスレッドがif
ステートメントに入る可能性はありますか?はい、しかし技術的にはvolatile
キーワードdoesではありません;副作用としてその結果がありますが、-およびvolatile
のほとんどの使用法はforその副作用です。実際、volatile
のMSDNドキュメントには、この副作用シナリオのみがリストされています( link )-実際のオリジナル言葉遣い(命令の並べ替えについて)が混乱しすぎていませんか?だから多分これはが今公式の使用法ですか?
bool
にはInterlocked
メソッドがありません。 0
/1
のような値を持つint
を使用する必要がありますが、それはほとんどbool
と同じですとにかく-Thread.VolatileRead
も機能することに注意してください
lock
には完全なフェンスがあります。そこに追加の構造は必要ありません。lock
だけで、JITが必要なものを理解するのに十分です。
個人的には、volatile
を使用します。オーバーヘッドの増加順に1/2/3をリストしました。ここでは、volatile
が最も安価なオプションになります。
might boolフラグにvolatile
キーワードを使用しますが、フィールドへのスレッドセーフアクセスを常に保証するわけではありません。
あなたの場合、おそらく別のクラスWorker
を作成し、バックグラウンドタスクの実行が完了したときに通知するイベントを使用します。
// Change this class to contain whatever data you need
public class MyEventArgs
{
public string Data { get; set; }
}
public class Worker
{
public event EventHandler<MyEventArgs> WorkComplete = delegate { };
private readonly object _locker = new object();
public void Start()
{
new Thread(DoWork).Start();
}
void DoWork()
{
// add a 'lock' here if this shouldn't be run in parallel
Thread.Sleep(5000); // ... massive computation
WorkComplete(this, null); // pass the result of computations with MyEventArgs
}
}
class MyClass
{
private readonly Worker _worker = new Worker();
public MyClass()
{
_worker.WorkComplete += OnWorkComplete;
}
private void OnWorkComplete(object sender, MyEventArgs eventArgs)
{
// Do something with result here
}
private void Update()
{
_worker.Start();
}
}
必要に応じてコードを自由に変更してください
追伸揮発性はパフォーマンスの点で優れており、シナリオでは、正しい順序で読み取りと書き込みを行うように見えるため、動作するはずです。おそらくメモリバリアは、新たに読み書きすることによって正確に達成される可能性がありますが、MSDNの仕様による保証はありません。 volatile
を使用するリスクを負うかどうかを決めるのはあなた次第です。
スレッドのIsAlive()メソッドを使用すると同じ動作を実現できるため、_done変数さえ必要ないかもしれません。 (バックグラウンドスレッドが1つしかない場合)
このような:
if(_thread == null || !_thread.IsAlive())
{
_thread = new Thread(Work);
_thread.Start();
}
私はこれをテストしませんでした..これは単なる提案です:)
System.Threading.Thread.MemoryBarrier()
はここで適切なツールです。コードは不格好に見えるかもしれませんが、他の実行可能な代替手段よりも高速です。
_bool _isDone = false;
public bool IsDone
{
get
{
System.Threading.Thread.MemoryBarrier();
var toReturn = this._isDone;
System.Threading.Thread.MemoryBarrier();
return toReturn;
}
private set
{
System.Threading.Thread.MemoryBarrier();
this._isDone = value;
System.Threading.Thread.MemoryBarrier();
}
}
_
volatile
は古い値の読み取りを妨げないため、ここでの設計目標を満たしていません。詳細については、 Jon Skeetの説明 または C#のスレッド化 を参照してください。
volatile
mayは、未定義の動作、特に多くの一般的なシステムの強力なメモリモデルが原因で、多くの場合動作するように見えることに注意してください。ただし、未定義の動作に依存すると、他のシステムでコードを実行しているときにバグが発生する可能性があります。この実用的な例は、このコードをRaspberry Piで実行している場合です(.NET Coreにより可能になりました!)。
Edit:「volatile
はここでは機能しない」という主張について議論した後、C#の仕様が保証するものは明確ではありません。おそらく、volatile
might遅延は大きくなりますが、動作が保証されます。 MemoryBarrier()
は、より高速なコミットを保証するため、依然として優れたソリューションです。この動作は、「 なぜメモリバリアが必要なのですか? 」で説明されている「NutshellのC#4」の例で説明されています。
ロックは、プロセス制御を強化するためのより重いメカニズムです。このようなアプリケーションでは不必要に不格好です。
パフォーマンスへの影響は十分に小さいため、軽度の使用ではおそらく気付かないでしょうが、依然として最適ではありません。また、スレッドの枯渇やデッドロックなどの大きな問題に貢献する可能性があります(わずかであっても)。
この問題を実証するために、ここに Microsoftの.NETソースコード(ReferenceSource経由) を示します。
_public static class Volatile
{
public static bool Read(ref bool location)
{
var value = location;
Thread.MemoryBarrier();
return value;
}
public static void Write(ref byte location, byte value)
{
Thread.MemoryBarrier();
location = value;
}
}
_
したがって、あるスレッドが__done = true;
_を設定し、別のスレッドが__done
_を読み取って、それがtrue
かどうかをチェックするとします。インライン化するとどうなりますか?
_void WhatHappensIfWeUseVolatile()
{
// Thread #1: Volatile write
Thread.MemoryBarrier();
this._done = true; // "location = value;"
// Thread #2: Volatile read
var _done = this._done; // "var value = location;"
Thread.MemoryBarrier();
// Check if Thread #2 got the new value from Thread #1
if (_done == true)
{
// This MIGHT happen, or might not.
//
// There was no MemoryBarrier between Thread #1's set and
// Thread #2's read, so we're not guaranteed that Thread #2
// got Thread #1's set.
}
}
_
要するに、volatile
の問題は、does insert MemoryBarrier()
's、it does n't必要な場所に挿入することですこの場合はそれら。
初心者は、以下を実行することでUnityでスレッドセーフコードを作成できます。
この方法では、コードにロックと揮発性を必要とせず、2つのディスパッチャ(すべてのロックと揮発性を隠す)だけが必要です。
これはシンプルで安全なバリアントであり、初心者が使用する必要があります。あなたはおそらく、専門家が何をしているのだろうと思っているでしょう。
これが私のプロジェクトのUpdate
メソッドのコードです。これはあなたが解決しようとしているのと同じ問題を解決します:
Helpers.UnityThreadPool.Instance.Enqueue(() => {
// This work is done by a worker thread:
SimpleTexture t = Assets.Geometry.CubeSphere.CreateTexture(block, (int)Scramble(ID));
Helpers.UnityMainThreadDispatcher.Instance.Enqueue(() => {
// This work is done by the Unity main thread:
obj.GetComponent<MeshRenderer>().material.mainTexture = t.ToUnityTexture();
});
});
上記のスレッドセーフを実現するために行う必要があるのは、エンキューを呼び出した後にblock
or ID
を編集しないことだけです。揮発性または明示的なロックは含まれません。
UnityMainThreadDispatcher
からの関連メソッドは次のとおりです。
List<Action> mExecutionQueue;
List<Action> mUpdateQueue;
public void Update()
{
lock (mExecutionQueue)
{
mUpdateQueue.AddRange(mExecutionQueue);
mExecutionQueue.Clear();
}
foreach (var action in mUpdateQueue) // todo: time limit, only perform ~10ms of actions per frame
{
try {
action();
}
catch (System.Exception e) {
UnityEngine.Debug.LogError("Exception in UnityMainThreadDispatcher: " + e.ToString());
}
}
mUpdateQueue.Clear();
}
public void Enqueue(Action action)
{
lock (mExecutionQueue)
mExecutionQueue.Add(action);
}
そして、Unityが最終的に.NET ThreadPoolをサポートするまで使用できるスレッドプール実装へのリンクを次に示します。 https://stackoverflow.com/a/436552/161274