Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

Multithreading in Unity ... works?

Discussion in 'Scripting' started by WrexialMT, Dec 31, 2020.

  1. WrexialMT

    WrexialMT

    Joined:
    Mar 28, 2013
    Posts:
    7
    Hey all,

    I'm here asking about a peculiar behaviour that... somehow... is working when every document online regarding unity and multithreading says it shouldn't be. Attached below you'll find some stripped down code that reproduces this case and I'm using Unity 2019.4.13f1 and targetting a windows build.

    For simplicity sake, I'm creating a thread and a coroutine, and each of these is spawning a gameobject, accessing and modify the gameobject's transform position. This, should NOT be allowed in thread according to all online sources.

    This is true, for development mode builds where the following error shows up as expected.
    https://pastebin.com/ieRRcvx2

    What I was not expecting is, for some reason, this behaviour does not occur on non-development builds (live builds) and the unity engine is allowing me to access it from a secondary thread.
    https://pastebin.com/GaHsjYCA

    My question is, why is this a thing and no one has talked about it? Is there anyone at unity that can confirm if this is a (unintentionally good?) bug?


    Code (CSharp):
    1. using System.Collections;
    2. using System.Threading;
    3. using UnityEngine;
    4.  
    5. namespace MultithreadedTest
    6. {
    7.     public class MultithreadedSpawnTestScript : MonoBehaviour
    8.     {
    9.         public void Start()
    10.         {
    11.             Debug.Log("Main Thread:" + Thread.CurrentThread.ManagedThreadId);
    12.  
    13.             var thread = new Thread(ThreadTimer);
    14.             thread.Start();
    15.  
    16.             StartCoroutine(CoroutineTimer());
    17.         }
    18.  
    19.         private void ThreadTimer()
    20.         {
    21.             Thread.Sleep(10);
    22.             Debug.Log("Multi Thread:" + Thread.CurrentThread.ManagedThreadId);
    23.             Spawn();
    24.         }
    25.  
    26.         private IEnumerator CoroutineTimer()
    27.         {
    28.             yield return new WaitForSeconds(5f);
    29.             Debug.Log("Coroutine Thread:" + Thread.CurrentThread.ManagedThreadId);
    30.             Spawn();
    31.         }
    32.  
    33.         public void Spawn()
    34.         {
    35.             Debug.Log("Spawning on Thread:" + Thread.CurrentThread.ManagedThreadId);
    36.             var spawnedObject = new GameObject("Spawned Dynamically").AddComponent<QuickTestComponent>();
    37.             spawnedObject.transform.position = new Vector3(Random.Range(0f, 10f), Random.Range(0f, 10f), Random.Range(0f, 10f));
    38.         }
    39.     }
    40.  
    41.     public class QuickTestComponent : MonoBehaviour
    42.     {
    43.         public void Start()
    44.         {
    45.             Debug.Log("Child's Start on Thread:" + Thread.CurrentThread.ManagedThreadId);
    46.             Debug.Log($"I WAS SPAWNED at {transform.position.ToString()}");
    47.         }
    48.     }
    49. }
     
    goblinmod, dreasgrech and Walki like this.
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,724
    Really surprised this is working. I wouldn't trust it for production code though. The GameObject API is definitely not thread safe. So while it might work in a small test environment, I'd guess you'll run into all kinds of mysterious bugs if you tried to do this for real, stemming from multithreaded access to components which were not designed for such.

    My educated guess is that this is enforced by some
    if (current thread is not main thread) show error;
    code that is stripped out by preprocessor directives for production builds for performance reasons.
     
    Last edited: Dec 31, 2020
  3. WrexialMT

    WrexialMT

    Joined:
    Mar 28, 2013
    Posts:
    7
    This was one of my initial guesses too, but if that would be the case, I would've assumed the actual spawning of the object to fail silently without any errors and not go through and spawn the object.
     
  4. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Well, sometimes you can do stuff with multithreading that is a problem waiting to happen: everything seems fine until two threads attempt to write the same memory address at the same time, or a value changes between being read and being written. Tragedy happens. The fact that a feature doesn't support multithreading doesn't necessarily mean that it will fail silently, or that it'll throw an error; it can just mean that things could go horribly wrong.
     
    Bunny83, WrexialMT and PraetorBlue like this.
  5. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,539
    Right, when it comes to multithreading I always like to quote this from Deep Water Horizon:

    Hope ain’t a tactic.

    Hoping for the best is just completely misplaced in multithreading code. Do it right or not at all -.-
     
    Suddoha and WrexialMT like this.
  6. dreasgrech

    dreasgrech

    Joined:
    Feb 9, 2013
    Posts:
    205
    I am getting the same results as OP. I am running these tests on a Windows build using Unity 2020.2.0a21.2837.

    This is the script I used for the test:
    Code (CSharp):
    1.  
    2. public class UnityThreadTester : MonoBehaviour
    3. {
    4.     public GameObject prefab;
    5.  
    6.     void Awake()
    7.     {
    8.         var file = File.CreateText(@"D:\dreasthread2.txt");
    9.         file.WriteLine("Random: " + Random.Range(0f, 42f));
    10.         file.WriteLine("main thread: " + Thread.CurrentThread.ManagedThreadId);
    11.  
    12.         // var y = Instantiate(prefab);
    13.         Write(file, "<AWAKE> Current Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    14.         new Task(() =>
    15.         {
    16.             try
    17.             {
    18.                 Write(file, "<TASK> Current Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    19.                 var y = Instantiate(prefab);
    20.                 Write(file, "<TASK> After instantiate");
    21.             }
    22.             catch (Exception ex)
    23.             {
    24.                 file.WriteLine(ex.Message);
    25.             }
    26.             finally
    27.             {
    28.                 file.Close();
    29.             }
    30.         }).Start();
    31.     }
    32.  
    33.     void Write(StreamWriter file, string text)
    34.     {
    35.         file.WriteLine(text);
    36.         Debug.Log(text);
    37.     }
    38. }
    This is the output with a Non-Development production build:


    And the output with a Development build:


    Educated guesses, speculations and assumptions and movie quotes are all cute but it would be really nice to know if there's a specific optimization we should be aware of when running in a production build as opposed to a development build.

    Does Unity have it's form of internal dispatcher that is only activated in a production build? If so, do we still need to add our own dispatcher on top of that?

    Multi-threading is already hard enough to work with and these inconsistencies between builds doesn't make it any easier.
     
    WrexialMT likes this.
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,539
    Multithreading is not hard or difficult. However multithreading has zero space for assumptions or speculations which is what you are doing here. The general rule is: If you don't know if a method is thread safe or not, the only valid assumption you can make is that it is not thread safe. Thread safety can not be tested conclusively through experiments, so don't even try it.

    The majority of the Unity API always has been single threaded and was not thread safe. Unity specifically implemented thread checks in most methods to inform the user that this method can only be used from the main thread. So my guess would be that they simply removed that check from release builds since the thread check is an unnecessary overhead in a release build. The check was / is mainly a tool for you, the developer, to realise that you can not use this method from another thread.

    Usually any code that is not thread safe can not tell you this fact. For example the generic List class is not thread safe (again the normal assumption holds true: if not explicitly stated any code should be considered unsafe). If you mess around with a List instance from two or more threads anyways, you don't get any warning or error, neither from the compiler nor the runtime. What you do get is unpredictable behaviour. This can result in data corruption / race conditions if you're lucky and in exceptions or crashes in the worst case.

    So unless you find a Unity documentation page that explicitly says that you can now use Instantiate from another thread, I would highly recommend to not do it.

    On the other hand you will find countless of examples where it's stated that the Unity API is generally not thread safe. Like this one.
     
  8. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    Maybe they changed it so you can opt-in or opt-out from the non-main thread access error checks? Yes, most of the API is not thread safe (and spawning GameObjects definitely shouldn't be), but a few things can be safe, if you really, really, know what you're doing. Actual background threads are a big no-no, but a few specific read-only "parallel for" scenarios are actually viable.
     
  9. dreasgrech

    dreasgrech

    Joined:
    Feb 9, 2013
    Posts:
    205
    Maybe it isn't hard for god programmers such as yourself but for us lowly mortals down here, it can actually be difficult. The reason I'm commenting on this post is because I want to do the opposite of speculating; I am searching for a definite answer like the OP so that I don't assume or guess anything.

    I haven't found a source that specifies you can use Instantiate from another thread, which is also the reason why I'm interested in this question in the hope of finding someone who can shed more light on the issue. But maybe someone else has information that can help us determine why this is happening.

    Possibly but now we're back to guessing. Does someone have a reference for that?
     
    Walki and WrexialMT like this.
  10. WrexialMT

    WrexialMT

    Joined:
    Mar 28, 2013
    Posts:
    7
    dreasgrech likes this.
  11. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,539
    No, we're not. The guessing was about thread safety, not thread unsafety which is the default. I've linked concrete statements from Unity which said that the Unity API is not thread safe. I work with Unity for 10 years now and it always has been that way. So I don't see any reason why we should assume it's now thread safe. The reason why you don't get an explicit error only in a release build is also quite common as most release build configurations remove unnecessary checks. So a bug report is completely overkill.

    I thought that this thread has been concluded long ago. We have countless of documentations, posts and statements that the API is not thread safe and the only counter argument you have is you don't get any error when you try it. I've already explained that you CAN NOT TEST for thread safety conclusively so any try and error setup is pointless.

    Since the Unity editor and the development build still throws an exception should be indication enough that the API is still not thread safe. Again assumptions about thread safety are dangerous, assumptions about thread unsafety are actually good / the normal case.
     
  12. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    Probably: Someone has talkaed about it but that discussion has been lost.
    Possibly: No one has ignored the error message during development and tried to do something they're told won't work in production.
    Maybe: The behaviour has changed recently because Unity is trying to make the API more thread safe but isn't prepared to support that yet.

    Having someone who works at Unity is about your only chance for an official answer, but my experience is that staff members only post here occasionally.



    It's well known to the community that the Unity API does not support being called by non-main threads, but if you need "proof", the link from Bunny83 seems official enough. This means, that in order to be supported, yes, you need to add a dispatcher of your own.

    Keep in mind that "supported" and "working" are different things. I've found you can often get a system to work in unsupported scenarios, just don't expect any help when it falls down.