33
\$\begingroup\$

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?

\$\endgroup\$
  • 1
    \$\begingroup\$ You can use the threading system to run other jobs in the background... but they may not call anything in the Unity API since that stuff is not threadsafe. Just commenting because I have not done this and can't confidently give you example code quickly. \$\endgroup\$ – Almo Dec 16 '15 at 3:54
  • \$\begingroup\$ Use List of Actions to store the functions you want to call in the main thread. lock and copy the Action list in the Update function to temporary list, clear the original list then execute the Action code in that List on the main thread. See UnityThread from my other post on how to do this. For example, to call a function on the main Thread, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); }); \$\endgroup\$ – Programmer Nov 16 '18 at 17:51
46
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Is there a way to use coroutines to yield only after the iteration takes more than 20ms? \$\endgroup\$ – DarkDestry Dec 16 '15 at 6:24
  • \$\begingroup\$ @DarkDestry You could use a stopwatch instance to time how long you're spending in your inner loop, and break out to an outer loop where you yield before resetting the stopwatch and resuming the inner loop. \$\endgroup\$ – DMGregory Dec 16 '15 at 6:27
  • 1
    \$\begingroup\$ Good answer! I just want to add that one more reason to use coroutines is that if you ever need to build for webgl that will work which it will not if you use threads. Sitting with that headache in a huge project right now :P \$\endgroup\$ – Mikael Högström May 5 '16 at 21:22
0
\$\begingroup\$

You can put your heavy calculation into another thread, but the Unity's API is not thread safe, you have to execute them in main thread.

Well you can try this package on Asset Store will help you to using threading easier. http://u3d.as/wQg You can simply use only one line of code to start a thread and execute Unity API safely.

\$\endgroup\$
0
\$\begingroup\$

Using Svelto.Tasks, you can return the result of a multi threaded routine to the main thread (and therefore unity functions) quite easily:

http://www.sebaslab.com/svelto-taskrunner-run-serial-and-parallel-asynchronous-tasks-in-unity3d/

\$\endgroup\$
0
\$\begingroup\$

@DMGregory explained it really well.

Both threading and coroutines can be used. Former to offload main thread and later to return control to main the thread. Pushing heavy lifting to seprate thread seems more reasonable. Therefore, job queues might be what you are after.

There is really good JobQueue example script on Unity Wiki.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.