如何为Unity3D编写线程安全的C#代码?

Sta*_* BZ 23 c# multithreading unity-game-engine

我想了解如何编写线程安全代码.

例如,我在我的游戏中有这个代码:

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;
}
Run Code Online (Sandbox Code Playgroud)

如果我理解正确,可能会发生主游戏线程和我_thread可以拥有自己的缓存版本_done,并且一个线程可能永远不会_done在另一个线程中看到更改?

如果可能,如何解决?

  1. 是否可以解决,只应用volatile关键字.

  2. 或者是有可能读取和写入,通过价值Interlocked的类似的方法ExchangeRead

  3. 如果我包围_done读写操作lock (_someObject),需要我使用Interlocked什么东西来防止缓存?

编辑1

  1. 如果我定义_doneas volatileUpdate从多个线程调用方法.if在分配_done给false 之前,2个线程是否可能输入语句?

Mar*_*ell 15

  1. 是的,但从技术上讲,这不是volatile关键字的作用 ; 然而,它具有副作用的结果 - 并且大多数用途volatile都是为了副作用; 实际上volatile现在的MSDN文档只列出了这个副作用场景(链接) - 我想实际的原始措辞(关于重新排序指令)太混乱了?所以也许这目前官方的使用情况如何?

  2. 没有bool方法Interlocked; 你需要使用一个int与像值0/ 1,但是这几乎是什么bool反正 -请注意,Thread.VolatileRead也将工作

  3. lock有一个完整的围栏; 你不需要任何额外的构造,lock它本身就足以让JIT理解你需要什么

就个人而言,我会使用volatile.您已经方便地列出了1/2/3增加的开销顺序.volatile将是这里最便宜的选择.


Fab*_*jan 11

虽然您可以使用volatile关键字作为bool标志,但它并不总能保证对该字段的线程安全访问.

在你的情况下,我可能会创建一个单独的类,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();
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以根据需要随意更改代码

PS Volatile在性能方面表现良好,在您的场景中,它应该工作,因为它看起来像是以正确的顺序进行读写.可能通过新读/写精确地实现了内存屏障 - 但MSDN规范无法保证.由您决定是否承担使用volatile或不使用的风险.

  • @StasBZ请参阅"他们可以互换吗?" 表格[在C#中的线程](http://www.albahari.com/threading/part4.aspx).__*tl; dr*-__`volatile`不会阻止读取尝试在写入更新的值后"读取"缓存的值,因此如果`_done`设置为`TRUE`,`volatile`将不会防止它显得"错误".(我不赞同这个答案的其余部分,但是关于那一点,海报是正确的.) (3认同)
  • @Shaggi,在ARM上你没有这个保证,C#确实运行它.Volatile不保证缓存一致性,因此您最终可能会读取过时值. (2认同)

小智 6

也许您甚至不需要_done变量,因为如果使用线程的IsAlive()方法,您可以实现相同的行为.(假设你只有1个后台线程)

像这样:

if(_thread == null || !_thread.IsAlive())
{
    _thread = new Thread(Work);
    _thread.Start();
}
Run Code Online (Sandbox Code Playgroud)

我没有测试这个顺便说一句..这只是一个建议:)


Nat*_*Nat 5

使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

不要使用挥发性物质

volatile不会阻止读取旧值,因此它不符合此处的设计目标.有关更多信息,请参阅Jon Skeet的解释C#中的Threading.

请注意,由于未定义的行为,在许多情况下volatile 可能会起作用,特别是在许多常见系统上的强内存模型.但是,当您在其他系统上运行代码时,依赖未定义的行为可能会导致出现错误.一个实际的例子就是你在Raspberry Pi上运行这个代码(现在可能是因为.NET Core!).

编辑:在讨论了" volatile在这里不起作用" 的说法后,目前还不清楚C#规范究竟保证了什么; 可以说,volatile 可能会保证有效,尽管它有更大的延迟. MemoryBarrier()仍然是更好的解决方案,因为它确保更快的提交.在" 为什么我需要内存屏障? "中讨论的"果壳中的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

所以,假设一个线程设置_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.
    }
}
Run Code Online (Sandbox Code Playgroud)

简而言之,问题volatile在于,虽然它确实插入了MemoryBarrier(),但在这种情况下它不会将它们插入我们需要它们的地方.