How to write thread-safe C# code for Unity3D?
Solution 1
yes, but technically that's not what the
volatile
keyword does; it has that result as a side-effect, though - and most uses ofvolatile
are for that side-effect; actually the MSDN documentation ofvolatile
now only lists this side-effect scenario (link) - I guess the actual original wording (about reordering instructions) was just too confusing? so maybe this is now the official usage?there are no
bool
methods forInterlocked
; you'd need to use anint
with values like0
/1
, but that's pretty much what abool
is anyway - note thatThread.VolatileRead
would also worklock
has a full fence; you don't need any additional constructs there, thelock
by itself is enough for the JIT to understand what you need
Personally, I'd use the volatile
. You've conveniently listed your 1/2/3 in increasing overhead order. volatile
will be the cheapest option here.
Solution 2
Although you might use volatile
keyword for your bool flag, it does not always guarantee a thread-safe access to the field.
In your case I'd probably create a separate class Worker
and use events to notify when background task completes execution:
// 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();
}
}
Feel free to change the code according to your needs
P.S.
Volatile is good performance-wise, and in your scenario it should work as it looks like you get your reads and writes in the right order. Possibly the memory barrier is achieved precisely by reading/writing freshly - but there is no guarantee by MSDN specifications. It's up to you to decide whether to take the risk of using volatile
or not.
Solution 3
Maybe you don't even need your _done variable, as you could achieve the same behaviour if you use the thread's IsAlive()-method. (given that you only have 1 background thread)
Like this:
if(_thread == null || !_thread.IsAlive())
{
_thread = new Thread(Work);
_thread.Start();
}
I didn't test this btw .. this is just a suggestion :)
Solution 4
Use MemoryBarrier()
System.Threading.Thread.MemoryBarrier()
is the right tool here. The code may look clunky, but it's faster than the other viable alternatives.
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();
}
}
Don't use volatile
volatile
doesn't prevent an older value from being read, so it doesn't meet the design objective here. See Jon Skeet's explanation or Threading in C# for more.
Note that volatile
may appear to work in many cases due to undefined behavior, specifically a strong memory model on many common systems. However, reliance on undefined behavior can cause bugs to appear when you're running your code on other systems. A practical example of this would be if you're running this code on a Raspberry Pi (now possible due to .NET Core!).
Edit: After discussing the claim that "volatile
won't work here", it's unclear exactly what the C# spec guarantees; arguably, volatile
might be guaranteed to work, albeit it with a larger delay. MemoryBarrier()
is still the better solution since it ensures a faster commit. This behavior is explained in an example from "C# 4 in a Nutshell", discussed in "Why do I need a memory barrier?".
Don't use locks
Locks are a heavier mechanism meant for stronger process control. They're unnecessarily clunky in an application like this.
The performance hit is small enough that you probably won't notice it with light use, but it's still sub-optimal. Also, it can contribute (even if slightly) toward larger problems like thread starvation and deadlocking.
Details about why volatile doesn't work
To demonstrate the issue, here's the .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;
}
}
So, say that one thread sets _done = true;
, then another reads _done
to check if it's true
. What does that look like if we inline it?
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.
}
}
In short, the problem with volatile
is that, while it does insert MemoryBarrier()
's, it doesn't insert them where we need them in this case.
Solution 5
Novices can create thread-safe code in Unity by doing the following:
- Copy the data they want a worker thread to work on.
- Tell a worker thread to work on the copy.
- From the worker thread, when the work is done, dispatch a call to the main thread to apply the changes.
This way you don't need locks and volatiles in your code, just two dispatchers (which hide all the locks and volatiles).
Now this is the simple and safe variant, the one novices should use. You're probably wondering what experts are doing: They do the exact same thing.
Here's some code from the Update
method in one of my projects, which solves the very same problem you're trying to solve:
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();
});
});
Notice the only thing we have to do to make the above thread safe is to not edit block
or ID
after calling enqueue. No volatile or explicit lock involved.
Here are the relevant methods from the 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);
}
And here's a link to a thread pool implementation you can use until Unity finally supports the .NET ThreadPool: https://stackoverflow.com/a/436552/1612743
Related videos on Youtube
Comments
-
Stas BZ almost 2 years
I'd like to understand how to write thread safe code.
For example I have this code in my game:
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; }
If I understand it correctly, it may happened that main game thread and my
_thread
can have its own cached version of_done
, and one thread may never see that_done
changed in another thread?And if it may, how to solve it?
Is it possible to solve by, only applying
volatile
keyword.Or is it possible to read and write value through
Interlocked
's methods likeExchange
andRead
?If I surround
_done
read and write operation withlock (_someObject)
, need I useInterlocked
or something to prevent caching?
Edit 1
- If I define
_done
asvolatile
and callUpdate
method from multiple threads. Is it possible that 2 threads will enterif
statement before I assign_done
to false?
-
M. Adeel Khalid about 7 yearsInstead of using
threads
, why don't you try it usingtasks
? -
Stas BZ about 7 years@MAdeelKhalid, I Use
Unity3d
engine, it supports only framework 2 -
PJvG about 7 years@StasBZ you can add tags
Unity3D
and the tag for the .NET version (e.g..net-3.5
) your using to avoid getting answers not applicable for your .NET version. -
Marc Gravell about 7 yearsNote that threads are expensive (relatively speaking) starting a new thread each time is probably not ideal
-
PJvG about 7 years@StasBZ Perhaps you should also look into using thread pools.
-
Programmer about 7 yearsI see what you are doing. You simply want to execute something in the main
Thread
from anotherThread
. What you have now is a basic way of doing that but it shouldn't be used in a production code. You need a queue system. Take a took at this post for proper way to do this. I made aUnityThread
class for that. -
Kroltan about 7 yearsIf you want to use multithreading, I'd recommend using a library. For example, UniRX provides a neat thread pool and tasks replacement for Unity, and also comes with all the Rx toolset (as the name implies, huh), which helps a lot in getting asynchronicity right.
-
Nat about 7 yearsUse the
public SharedState GetFreshSharedState { get; }
implementation shown in stackoverflow.com/questions/24677773/… with.MemoryBarrier()
. Don't usevolatile
because it has a fencing issue. Don't uselock
because it's unnecessarily clunky for this application. You could use.Interlocked()
with anint
if you're willing to check it against aconst int TRUE_VALUE = 1
, or something like that, to turn it into abool
. -
Nat about 7 yearsSee the "Can they be swapped?" table in Threading in C# for an explanation about why
volatile
won't fully solve this problem. -
Nat about 7 years
-
Stas BZ about 7 yearsCan you explain why
volatile
does not guarantee thread-safe access to the field? Or share link. -
Stas BZ about 7 yearsCould you answer position 4 that I added to my question, please?
-
Nat about 7 years@StasBZ See the "Can they be swapped?" table in Threading in C#. tl;dr-
volatile
doesn't prevent a read attempt from "reading" a cached value after a newer one was written, so if_done
is set toTRUE
,volatile
won't prevent it from appearing to beFALSE
. (I'm not endorsing the rest of this answer, but the poster was right about that particular point.) -
Nat about 7 years
-
IInspectable about 7 years"Unless Unity's version of Mono really does something hugely and codebreakingly different from the regular .Net Framework [...], you'll not have different copies of _done in different threads in your scenario." - There is a world outside the framework, namely a CPU that runs the code. The CPU could store
_done
in a register, and with each thread maintaining a separate copy of the register file, you could wind up with two distinct copies of_done
in two threads. -
Benjamin Gruenbaum about 7 yearsThis is the correct way to solve the problem. No need to do synchronization manually when it is provided by the platform. Writing multithreaded code is hard enough as it is.
-
Peter about 7 yearsThanks. To prevent a potential misunderstanding: Unfortunately the solution isn't part of the official platform yet,
UnityMainThreadDispatcher
andUnityThreadPool
are classes that every shop has to re-create on their own. Although I did provide a partial implementation for one and a link to the other, to ease the pain. I'm currently considering posting the complete but less than perfect versions of both as a community wiki answer. -
Shaggi about 7 yearsThe link explains reordering inside one thread, not across threads. Acquire/release semantics establish synchronization primitives between multiple threads. The author in the link is wrong (as is the assertion in this answer): Whether you're reading a fresh or stale value atomically is a question of cache-coherency, not memory ordering - also, most processors (including all platforms C# run on) guarantee cache coherency by default.
-
Shaggi about 7 yearsSee my other comment, as well. "Older values" (referred to typically as fresh or stale) are effects of cache-coherency, not memory ordering. If your volatile read/write are of the atomic type (hint: volatile type requirements are identical), volatile indeed always guaranteed the most fresh value. See §10.5 of the spec for more information.
-
creker about 7 years@Shaggi, on ARM you don't have that guarantee and C# does run on it. Volatile doesn't guarantee cache-coherency, so you might end up reading stale value.
-
creker about 7 years@Shaggi, the spec doesn't say anything about preventing stale values. It only mentions memory reordering. So you still have problem with cache-coherency and it will get you in trouble on something like ARM
-
creker about 7 yearsI don't like this answer. Locks should be the first and pretty much the only thing you use in cases like this. Just because they work and don't require too much expertise. If you don't want lock for some reason, you can use interlocked. If you still thinking that you need something else, then think again. You should really know what you're doing and why, if you thinking about placing memory barriers by hand.
-
Nat about 7 years@creker Locks are a conceptually incorrect tool here. They'll do weird stuff like prevent two threads from reading the
.IsDone
at the same time. Plus you have to pay for the undesired feature by taking an unnecessary performance hit, both for merely acquiring the lock in the first place and a larger one if you frequently encounter reads block each other. Finally, locks can lead to thread starvation under heavy load, freezing the program. -
Shaggi about 7 years@creker Reading up on this, it is clear that referring to "arm" as an architecture is meaningless, however they did change their cache coherence guarantees on all archs at some point to always have it. This 17-year old arch describes how you must (in software) update shared memory if you 'dirty' it... Not hard to see why everyone is moving towards cache coherence: infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0151c/… A9 (at least, and forward) has cache coherence: infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0407e/…
-
Shaggi about 7 yearsLet us continue this discussion in chat.
-
rs232 about 7 yearsIt is not a CPU who decides which variable should be stored in a register, but a compiler during optimization. I highly doubt that a compiler would ever move a class data member variable, which is used from withing multiple class methods, into a register.
-
IInspectable about 7 yearsI didn't imply, that the CPU were to make that decision (although it wasn't worded succinctly enough either). The point, however, is, that any variable must be loaded into a CPU register, if the CPU needs to work with it, creating at least a transient state, where two distinct copies exist.
-
rs232 about 7 yearsYou are now re-phrasing the second paragraph of my own answer.
-
IInspectable about 7 yearsYour first paragraph claims, that there never will be two copies in distinct memory locations, and your second paragraph never challenges this. My comments claim, that the variable can have two distinct copies in different locations (e.g. memory and CPU register). This contradicts your entire proposed answer, and frankly, I wouldn't know what to make of your second paragraph based on the assumption that you meant to have it agree with your first paragraph. Both cannot be right at the same time. Take 0 up-votes on a high-frequency Q&A as a hint.
-
rs232 about 7 yearsThe variable's value might be copied from memory to a register and back again as a part of a non-atomic operation, which is not something you might describe as "game thread and my _thread can have its own cached version of _done". When you consider these data operations as variable copies, you mix internal details of a realization of non-atomic operations on certain processors with language entities. From the user's code perspective, it absolutely does not matter what happens on hardware level.
-
rs232 about 7 yearsThe only important things are: a) both threads will access the same variable, no "cached versions" b) they should not access it at the same time, that's why critical sections required. I see you're frustrated being unable to separate different layers of abstraction; feel free to ask more questions if you still fail to understand what is the difference and why it is important.