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. Dismiss Notice

How to lock a Coroutine?

Discussion in 'Scripting' started by wojwen, May 15, 2021.

  1. wojwen

    wojwen

    Joined:
    Feb 13, 2018
    Posts:
    38
    I have a custom scene manager, which apart from loading scenes calls some events for me. I want it to fully load a scene, before it starts loading another one, so I added a lock. I'm using Monitor because I want to enter the lock in OnLoadScene() and exit it in LoadSceneAsync() and this wouldn't be possible with the lock() {} syntax, because LoadSceneAsync() is called as a Coroutine (asynchronously). I found the Monitor syntax here: C# manual lock/unlock.

    Here is the code:

    Code (CSharp):
    1. public class CustomSceneManager : Singleton<CustomSceneManager>
    2. {
    3.    public delegate void SceneChange(string sceneName);
    4.  
    5.    public event SceneChange LoadScene;
    6.    public event SceneChange UnloadScene;
    7.  
    8.    private static readonly object LoadSceneLock = new object();
    9.  
    10.    private IEnumerator LoadSceneAsync(string sceneName)
    11.    {
    12.        var asyncLoadLevel = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
    13.        while (!asyncLoadLevel.isDone)
    14.        {
    15.            yield return null;
    16.        }
    17.  
    18.        Debug.Log($"Finished loading: {sceneName}");
    19.        LoadScene?.Invoke(sceneName);
    20.        Monitor.Exit(LoadSceneLock); // exit lock
    21.    }
    22.  
    23.    public void OnLoadScene(string newSceneName)
    24.    {
    25.        Monitor.Enter(LoadSceneLock); // enter lock
    26.        Debug.Log($"Started loading: {newSceneName}");
    27.        UnloadScene?.Invoke(newSceneName);
    28.        StartCoroutine(LoadSceneAsync(newSceneName));
    29.    }
    30. }
    The issue is that it doesn't work as I expect it to. This is how my logs look:

    Started loading: scene_1
    Started loading: scene_2
    Finished loading: scene_1
    Finished loading: scene_2​

    Am I using the lock incorrectly?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,762
    I dunno what this schmancy Monitor stuff is... do you need that? Why don't you just use a boolean such as one named
    ready
    ? You clear it to false when you start loading, then when done set it to true.

    This is my usual pattern for async scene loading, and I use it on many scenes that all come together at once, such as to keep the LOADING screen in place until they're all ready, and to decide when to spawn the player, etc.

    https://forum.unity.com/threads/daily-events-and-content-changes.1108202/#post-7143713
     
  3. wojwen

    wojwen

    Joined:
    Feb 13, 2018
    Posts:
    38
    I can't use a flag because I need the function to block itself - so when it's called multiple times consecutively, then all calls will get executed one after another.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    You can't really thread lock like this in Unity... I mean you can, it's just there's ramifications.

    Unity expects the main thread to not lock because proceeding another frame needs the main thread to continue. Especially in the case of a Coroutine, the Coroutine ticks on the main thread, and you have to start a Coroutine on the main thread.

    So by doing this what will happen is that if Monitor.Enter blocks, well any running coroutines can't proceed, so Monitor.Exit will never be reached. Your entire game just stalls out.

    ...

    What is it you're attempting to do?

    I mean, you can queue things up. Just create a queue and process them as such:

    Code (csharp):
    1. public class CustomSceneManager : Singleton<CustomSceneManager>
    2. {
    3.     public delegate void SceneChange(string sceneName);
    4.  
    5.     public event SceneChange LoadScene;
    6.     public event SceneChange UnloadScene;
    7.  
    8.     private bool _loading;
    9.     private Queue<Action> _loadQueue = new Queue<Action>();
    10.  
    11.     private IEnumerator LoadSceneAsync(string sceneName)
    12.     {
    13.         var asyncLoadLevel = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
    14.         while (!asyncLoadLevel.isDone)
    15.         {
    16.             yield return null;
    17.         }
    18.  
    19.         Debug.Log($"Finished loading: {sceneName}");
    20.         LoadScene?.Invoke(sceneName);
    21.         if (_loadQueue.Count > 0)
    22.         {
    23.             _loadQueue.Dequeue().Invoke();
    24.         }
    25.         else
    26.         {
    27.             _loading = false;
    28.         }
    29.     }
    30.  
    31.     public void QueueSceneLoad(string newSceneName)
    32.     {
    33.         if(_loading)
    34.         {
    35.             _loadQueue.Enqueue(() => {
    36.                 _loading = true;
    37.                 Debug.Log($"Started loading: {newSceneName}");
    38.                 UnloadScene?.Invoke(newSceneName);
    39.                 StartCoroutine(LoadSceneAsync(newSceneName));
    40.             });
    41.         }
    42.         else
    43.         {
    44.             _loading = true;
    45.             Debug.Log($"Started loading: {newSceneName}");
    46.             UnloadScene?.Invoke(newSceneName);
    47.             StartCoroutine(LoadSceneAsync(newSceneName));
    48.         }
    49.     }
    50. }
    (note - this code was slap-dash written in the browser, I did not test it, it serves only as a demonstration of the concept. Do not copy paste it expecting it to "just work".)

    But I don't know what you're attempting to actually accomplish, so I don't know if this actually does what you need. I say this because in yours a call to OnLoadScene would block until the load completed (which is both counter to how Unity works, but an implication you were hoping to utilize from the calling context...). My code would not block when you call QueueSceneLoad.

    How you could get around that is to use say a 'Task' or some other token that is returned that can be monitored for completion. Again without blocking though because, you should never block/lock the main thread in Unity.

    What is your end goal here?
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Monitor is really the underlying mechanism that the lock keyword uses.

    Basically when you say:
    Code (csharp):
    1. lock(obj)
    2. {
    3.     //do stuff
    4. }
    It's syntax sugar for:
    Code (csharp):
    1. Monitor.Enter(obj);
    2. try
    3. {
    4.    //do stuff
    5. }
    6. finally
    7. {
    8.    Monitor.Exit(obj);
    9. }
    Effectively Monitor.Enter flags 'obj' as being taken at this moment, and releases it when you call Exit.

    If no one currently owns it, Enter immediately returns.

    If someone does own it, it blocks until Exit is called by the previous owner.

    It's basically the simplest form of thread locking that can exist... it's also a mechanism you should generally avoid since things like Mutex, WaitHandle, Tasks, etc all exist.

    OP appears to want to be able to begin a lock in one method, but exit it in another method. Effectively behaving the way a Mutex or WaitHandle does.

    I would have suggested OP use a Mutex/WaitHandle if it weren't for that fact that ANY blocking of the main thread is just bad.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,762
    For something as banal and bland as scene loading?! Really?! That doesn't seem necessary or wise.

    But you can't block the main thread in any case. Here's why:

    https://docs.unity3d.com/Manual/ExecutionOrder.html

    This is my go-to pattern for additive scenes, use it ALL the time:

    https://forum.unity.com/threads/right-way-for-performance-divide-scene.1023673/#post-6630961
    https://forum.unity.com/threads/right-way-for-performance-divide-scene.1023673/#post-6754330

    https://forum.unity.com/threads/problem-with-canvas-ui-prefabs.1039075/#post-6726169

    A multi-scene loader thingy:

    https://pastebin.com/Vecczt5Q

    My typical Scene Loader:

    https://gist.github.com/kurtdekker/862da3bc22ee13aff61a7606ece6fdd3

    Other notes on additive scene loading:

    https://forum.unity.com/threads/removing-duplicates-on-load-scene.956568/#post-6233406

    Timing of scene loading:

    https://forum.unity.com/threads/fun...ject-in-the-second-scene.993141/#post-6449718

    Also, if something exists only in one scene, DO NOT MAKE A PREFAB out of it. It's a waste of time and needlessly splits your work between two files, the prefab and the scene, leading to many possible errors and edge cases.

    Checking if everything is ready to go: (I already posted this above... it really works)

    https://forum.unity.com/threads/daily-events-and-content-changes.1108202/#post-7143713
     
  7. wojwen

    wojwen

    Joined:
    Feb 13, 2018
    Posts:
    38
    First of all, thanks for the answer lordofduct, this should fix my issue.

    I'm not doing additive scene loading. I've made this custom scene manager to be able to swap objects between DontDestroyOnLoad and the loaded/unloaded scene on load/unload (move to DDOL before loading and to the new scene after loading). The built-in Unity events didn't work for me - they got called before the scene was fully loaded and this caused errors. If there is a simpler solution please let me know, but for now I will have to stick with my solution.
     
  8. fgeorg_oculus

    fgeorg_oculus

    Joined:
    Aug 3, 2018
    Posts:
    1
    A coworker recently showed me an elegant solution to this problem. By keeping track of the caller order using "tickets" in a queue, you ensure that anything calling the coroutine is run in first-in-first-out order.

    Code (CSharp):
    1. private Queue<object> _ticketQueue = new Queue<object>();
    2. private IEnumerator MyCoroutine() {
    3.     var ticket = new object();
    4.     _ticketQueue.Enqueue(ticket);
    5.     while(_ticketQueue.Peek() != ticket) {
    6.         yield return null;
    7.     }
    8.  
    9.     // Do async work
    10.  
    11.     _ticketQueue.Dequeue();
    12. }
    As others stated, no locks are needed because coroutines always run on the main thread using enumerators, but you can also use this "ticket" recipe in concurrent async code if you change the Queue to ConcurrentQueue
     
    Last edited: Dec 7, 2021
    yeonsh likes this.