Wednesday, September 18, 2019

multithreading - How to not freeze the main thread in Unity?


I have a level generation algorithm that is computationally heavy. As such, calling it always results in the game screen freezing. How can I place the function on a second thread while the game still continues to render a loading screen to indicate the game is not frozen?



Answer



Update: In 2018, Unity is rolling out a C# Job System as a way to offload work and make use of multiple CPU cores.


The answer below predates this system. It will still work, but there may be better options available in modern Unity, depending on your needs. In particular, the job system appears to resolve some of the limitations on what manually-created threads can safely access, described below. Developers experimenting with the preview report performing raycasts and constructing meshes in parallel, for example.


I'd invite users with experience using this job system to add their own answers reflecting the current state of the engine.




I've used threading for heavyweight tasks in Unity in the past (usually image & geometry processing), and it's not drastically different than using threads in other C# applications, with two caveats:





  1. Because Unity uses a somewhat older subset of .NET, there are some newer threading features and libraries we can't use out of the box, but the basics are all there.




  2. As Almo notes in a comment above, many Unity types are not threadsafe, and will throw exceptions if you try to construct, use, or even compare them off the main thread. Things to keep in mind:




    • One common case is checking to see if a GameObject or Monobehaviour reference is null before trying to access its members. myUnityObject == null calls an overloaded operator for anything descended from UnityEngine.Object, but System.Object.ReferenceEquals() works around this to a degree - just remember that a Destroy()ed GameObject compares as equal to null using the overload, but is not yet ReferenceEqual to null.




    • Reading parameters from Unity types is usually safe on another thread (in that it won't immediately throw an exception as long as you're careful to check for nulls as above), but note Philipp's warning here that the main thread might be modifying state while you're reading it. You'll need to be disciplined about who's allowed to modify what & when in order to avoid reading some inconsistent state, which can lead to bugs that can be devillishly hard to track down because they depend on sub-millisecond timings between threads that we can't reproduce at will.





    • Random and Time static members are not available. Create an instance of System.Random per thread if you need randomness, and System.Diagnostics.Stopwatch if you need timing info.




    • Mathf functions, Vector, Matrix, Quaternion, and Color structs all work fine across threads, so you can do most of your computations separately




    • Creating GameObjects, attaching Monobehaviours, or creating/updating Textures, Meshes, Materials, etc. all need to happen on the main thread. In the past when I've needed to work with these, I've set up a producer-consumer queue, where my worker thread prepares the raw data (like a big array of vectors/colours to apply to a mesh or texture), and an Update or Coroutine on the main thread polls for data and applies it.







With those notes out of the way, here's a pattern I often use for threaded work. I make no guarantee that it's a best-practice style, but it gets the job done. (Comments or edits to improve are welcome - I know threading is a very deep topic of which I only know the basics)


using UnityEngine;
using System.Threading;

public class MyThreadedBehaviour : MonoBehaviour
{


bool _threadRunning;
Thread _thread;

void Start()
{
// Begin our heavy work on a new thread.
_thread = new Thread(ThreadedWork);
_thread.Start();
}



void ThreadedWork()
{
_threadRunning = true;
bool workDone = false;

// This pattern lets us interrupt the work at a safe point if neeeded.
while(_threadRunning && !workDone)
{
// Do Work...

}
_threadRunning = false;
}

void OnDisable()
{
// If the thread is still running, we should shut it down,
// otherwise it can prevent the game from exiting correctly.
if(_threadRunning)
{

// This forces the while loop in the ThreadedWork function to abort.
_threadRunning = false;

// This waits until the thread exits,
// ensuring any cleanup we do after this is safe.
_thread.Join();
}

// Thread is guaranteed no longer running. Do other cleanup tasks.
}

}

If you don't strictly need to split work across threads for speed, and you're just looking for a way to make it non-blocking so the rest of your game keeps ticking, a lighter-weight solution in Unity is Coroutines. These are functions that can do some work then yield control back to the engine to continue what it's doing, and resume seamlessly at a later time.


using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{

void Start()

{
// Begin our heavy work in a coroutine.
StartCoroutine(YieldingWork());
}

IEnumerator YieldingWork()
{
bool workDone = false;

while(!workDone)

{
// Let the engine run for a frame.
yield return null;

// Do Work...
}
}
}

This doesn't need any special cleanup considerations, since the engine (so far as I can tell) gets rid of coroutines from destroyed objects for you.



All the local state of the method is preserved when it yields and resumes, so for many purposes it's as though it was running uninterrupted on another thread (but you have all the conveniences of running on the main thread). You just need to make sure each iteration of it is short enough that it's not going to slow your main thread unreasonably.


By ensuring important operations aren't separated by a yield, you can get the consistency of single-threaded behaviour - knowing that no other script or system on the main thread can modify data you're in the middle of working on.


The yield return line gives you a few options. You can...



  • yield return null to resume after the next frame's Update()

  • yield return new WaitForFixedUpdate() to resume after the next FixedUpdate()

  • yield return new WaitForSeconds(delay) to resume after a certain amount of game time elapses

  • yield return new WaitForEndOfFrame() to resume after the GUI finishes rendering

  • yield return myRequest where myRequest is a WWW instance, to resume once the requested data finishes loading from the web or disc.



  • yield return otherCoroutine where otherCoroutine is a Coroutine instance, to resume after otherCoroutine completes. This is often used in the form yield return StartCoroutine(OtherCoroutineMethod()) to chain execution to a new coroutine that can itself yield when it wants to.




    • Experimentally, skipping the second StartCoroutine and simply writing yield return OtherCoroutineMethod() accomplishes the same goal if you want to chain execution in the same context.


      Wrapping inside a StartCoroutine may still be useful if you want to run the nested coroutine in association with a second object, like yield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())






...depending on when you want the coroutine to take its next turn.



Or yield break; to stop the coroutine before it reaches the end, the way you might use return; to early-out of a conventional method.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...