我想了解如何编写线程安全代码。
例如我在游戏中有这个代码:
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
方法。在将if
指定为false之前,2个线程是否可能会输入_done
语句?答案 0 :(得分:15)
是的,但从技术上讲,这不是volatile
关键字所做的;然而,它有这种结果作为副作用 - volatile
的大部分用法都是 这种副作用;实际上volatile
的MSDN文档现在只列出了这个副作用场景(link) - 我想实际的原始措辞(关于重新排序指令)太混乱了?那么这个现在是官方用法吗?
bool
没有Interlocked
个方法;您需要使用int
/ 0
之类的值1
,但这几乎是bool
/ em> - 请注意Thread.VolatileRead
也可以使用
lock
有一个完整的围栏;你不需要任何额外的结构,lock
本身足以让JIT了解你的需要
就个人而言,我使用volatile
。您已经方便地列出了1/2/3增加的开销顺序。 volatile
将是最便宜的选择。
答案 1 :(得分: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();
}
}
根据您的需要随意更改代码
P.S。
易失性是性能良好的,在您的场景中它应该工作,因为它看起来像您正确的顺序读取和写入。可能通过新读/写精确地实现了内存屏障 - 但MSDN规范无法保证。您可以自行决定是否承担使用volatile
的风险。
答案 2 :(得分:6)
也许您甚至不需要_done变量,因为如果使用线程的IsAlive()方法,您可能会遇到相同的行为。 (假设你只有1个后台线程)
像这样:
img
我没有测试过这个......这只是一个建议:)
答案 3 :(得分:5)
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's explanation或Threading in C#。
请注意volatile
可能在很多情况下由于未定义的行为而出现,特别是在许多常见系统上都是强大的内存模型。但是,当您在其他系统上运行代码时,依赖未定义的行为可能会导致出现错误。一个实际的例子就是你在Raspberry Pi上运行这个代码(现在可能是因为.NET Core!)。
修改:在讨论了“volatile
无法在此处发挥作用”的说法之后,目前还不清楚C#规范究竟保证了什么;可以说,volatile
可能可以保证工作,尽管它有更大的延迟。 MemoryBarrier()
仍然是更好的解决方案,因为它可以确保更快的提交。在“Why do I need a memory barrier?”中讨论的“果壳中的C#4”中的示例中解释了此行为。
锁是一种较重的机制,用于加强过程控制。他们在这样的应用程序中不必要地笨拙。
性能打击足够小,你可能不会注意到光线使用,但它仍然是次优的。此外,它可以(如果稍微)对线程饥饿和死锁等较大问题做出贡献。
为了证明这个问题,这里是.NET source code from Microsoft (via 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
的问题在于,当 插入MemoryBarrier()
时,它不会将它们插入在这种情况下我们需要它们。
答案 4 :(得分:4)
Novices可以通过执行以下操作在Unity中创建线程安全的代码:
这样你的代码就不需要锁和挥发物了,只需要两个调度员(隐藏所有的锁和挥发物)。
现在这是一个简单而安全的变体,一个新手应该使用。你可能想知道专家在做什么:他们完全一样。
以下是我的一个项目中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();
});
});
请注意,要使上述线程安全,我们唯一要做的就是在调用enqueue后不要编辑block
或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/1612743
答案 5 :(得分:-2)
我认为您不需要在代码中添加太多内容。除非Unity的Mono版本真的做了大量的事情并且与常规的.Net Framework(我确定它没有)有着不同的代码,你在场景中的不同线程中不会有_done的不同副本。所以,不需要互锁,不需要挥发,不需要黑魔法。
当你的主线程将后台线程设置为true的同一时刻你的主线程将_done设置为false时,你真正可能会遇到的情况非常高,这绝对是一件坏事。这就是为什么你需要用'lock'来包围那些操作(不要忘记将它们锁在SAME同步对象上)。
只要您按照代码示例中的描述使用_done,就可以在每次编写变量时“锁定”它,一切都会好的。