web-dev-qa-db-ja.com

Unity3DのスレッドセーフC#コードを記述する方法

スレッドセーフコードの記述方法を理解したいと思います。

たとえば、ゲームにこのコードがあります:

_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_が変更されたことを決して見ないことがありますか?

そして、もしそうなら、それを解決する方法は?

  1. volatileキーワードのみを適用して解決することは可能ですか?.

  2. または、InterlockedExchangeなどのReadのメソッドを使用して値を読み書きすることはできますか?

  3. __done_読み取りおよび書き込み操作をlock (_someObject)で囲む場合、Interlockedまたはキャッシュを防ぐために何かを使用する必要がありますか?

編集1

  1. __done_をvolatileとして定義し、Updateメソッドを複数のスレッドから呼び出した場合。 __done_をfalseに割り当てる前に、2つのスレッドがifステートメントに入る可能性はありますか?
23
Stas BZ
  1. はい、しかし技術的にはvolatileキーワードdoesではありません;副作用としてその結果がありますが、-およびvolatileのほとんどの使用法はforその副作用です。実際、volatileのMSDNドキュメントには、この副作用シナリオのみがリストされています( link )-実際のオリジナル言葉遣い(命令の並べ替えについて)が混乱しすぎていませんか?だから多分これは今公式の使用法ですか?

  2. boolにはInterlockedメソッドがありません。 0/1のような値を持つintを使用する必要がありますが、それはほとんどboolと同じですとにかく-Thread.VolatileReadも機能することに注意してください

  3. lockには完全なフェンスがあります。そこに追加の構造は必要ありません。lockだけで、JITが必要なものを理解するのに十分です。

個人的には、volatileを使用します。オーバーヘッドの増加順に1/2/3をリストしました。ここでは、volatileが最も安価なオプションになります。

15
Marc Gravell

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を使用するリスクを負うかどうかを決めるのはあなた次第です。

11
Fabjan

スレッドのIsAlive()メソッドを使用すると同じ動作を実現できるため、_done変数さえ必要ないかもしれません。 (バックグラウンドスレッドが1つしかない場合)

このような:

if(_thread == null || !_thread.IsAlive())
{
    _thread = new Thread(Work);
    _thread.Start();
}

私はこれをテストしませんでした..これは単なる提案です:)

6
Stefan Woehrer

MemoryBarrier()を使用します

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を使用しないでください

volatileは古い値の読み取りを妨げないため、ここでの設計目標を満たしていません。詳細については、 Jon Skeetの説明 または C#のスレッド化 を参照してください。

volatilemayは、未定義の動作、特に多くの一般的なシステムの強力なメモリモデルが原因で、多くの場合動作するように見えることに注意してください。ただし、未定義の動作に依存すると、他のシステムでコードを実行しているときにバグが発生する可能性があります。この実用的な例は、このコードをRaspberry Piで実行している場合です(.NET Coreにより可能になりました!)。

Edit:「volatileはここでは機能しない」という主張について議論した後、C#の仕様が保証するものは明確ではありません。おそらく、volatilemight遅延は大きくなりますが、動作が保証されます。 MemoryBarrier()は、より高速なコミットを保証するため、依然として優れたソリューションです。この動作は、「 なぜメモリバリアが必要なのですか? 」で説明されている「NutshellのC#4」の例で説明されています。

ロックを使用しないでください

ロックは、プロセス制御を強化するためのより重いメカニズムです。このようなアプリケーションでは不必要に不格好です。

パフォーマンスへの影響は十分に小さいため、軽度の使用ではおそらく気付かないでしょうが、依然として最適ではありません。また、スレッドの枯渇やデッドロックなどの大きな問題に貢献する可能性があります(わずかであっても)。

Volatileが機能しない理由の詳細

この問題を実証するために、ここに 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必要な場所に挿入することですこの場合はそれら。

5
Nat

初心者は、以下を実行することで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();
    });
});

上記のスレッドセーフを実現するために行う必要があるのは、エンキューを呼び出した後にblockor 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

4
Peter