Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice

Feedback 2022 Threading Improvement Wishlist

Discussion in '2022.1 Beta' started by TheZombieKiller, Oct 13, 2021.

  1. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    Now that Unity 2022 is out in alpha, I figure now is as good a time as any to look at new features or improvements. Working with multiple threads is a particular area that I feel could vastly benefit from a few minor additions:

    1. Make UnitySynchronizationContext a public type.
    This type is essential to queuing work on the main thread. While a similar type can be reimplemented manually, it would not be automatically called by the engine and has a number of complicated problems.

    2. Expose an accessor for UnitySynchronizationContext.
    At present, there is no way to specifically request the Unity synchronization context instance, you must instead try to capture the SynchronizationContext.Current value at a point where you believe it is set to the right instance. This is error-prone and can be broken by third party code, especially because there is no guaranteed execution order for [RuntimeInitializeOnLoadMethod] callbacks. All it takes is for some other code to call SynchronizationContext.SetSynchronizationContext before you can capture the instance for everything to fall apart.

    3. Make UnitySynchronizationContext.Exec (and others) public.
    When dealing with multi-threading in Unity, you can often find yourself in a situation where you have an API that waits on an operation that needs to dispatch to the main thread at some point. If the method waiting on this operation was called from the main thread, you have a deadlock. An example of such a situation looks like:
    Code (CSharp):
    1. var spin = new SpinWait();
    2.  
    3. while (condition)
    4.     spin.SpinOnce();
    5.  
    6. FinishOperation();
    This can be fixed by manually executing any pending tasks on the SynchronizationContext inside the loop (which currently requires reflection to call Exec):
    Code (CSharp):
    1. var spin = new SpinWait();
    2.  
    3. while (condition)
    4. {
    5.     if (UnityObjectInternal.CurrentThreadIsMainThread())
    6.         MainThreadContext.ExecutePendingTasks();
    7.  
    8.     spin.SpinOnce();
    9. }
    10.  
    11. FinishOperation();
    It would also make sense to expose the HasPendingTasks method and the main thread ID.

    4. Make Object.CurrentThreadIsMainThread and Object.EnsureRunningOnMainThread public.
    These methods are extremely useful for safety checks and performing optimizations (such as invoking a callback immediately if you're already on the main thread, rather than queuing it for execution later). If you want to avoid calling them via reflection, you need to make use of a [RuntimeInitializeOnLoadMethod] callback that will capture the Thread.CurrentThread instance, and hope that it is called before any other [RuntimeInitializeOnLoadMethod] callbacks that might need to know about the main thread.

    API Proposal
    Given all of the above changes, the new APIs would look something like the following:
    Code (CSharp):
    1. namespace UnityEngine
    2. {
    3.     public partial class Object
    4.     {
    5.         public static bool CurrentThreadIsMainThread { get; }
    6.         public static void EnsureRunningOnMainThread();
    7.     }
    8.  
    9.     public sealed class UnitySynchronizationContext : SynchronizationContext
    10.     {
    11.         public static UnitySynchronizationContext GetUnitySynchronizationContext();
    12.         public int ManagedThreadId { get; }
    13.         public bool HasPendingTasks { get; }
    14.         public void ExecutePendingTasks();
    15.     }
    16. }
     
    Last edited: Apr 2, 2022
  2. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,674
    While I'm not on the right team to comment on these proposals, I'd just like to point out that the "RawObject" layout is only going to be correct in the editor - the instanceId/UnityRuntimeErrorString fields don't exist in builds. So if you're using this code in current Unity versions, heads up :).
     
    TheZombieKiller likes this.
  3. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    Is the static "OffsetOfInstanceIDInCPlusPlusObject" field retained (and set) in builds? I could just read it from the native object in that case. If not, I do have the layout of the internal C++ class so I can just make use of that. (Edit) It appears I can use GetOffsetOfInstanceIDInCPlusPlusObject, I'll update the example in the OP to a more "safe" implementation using that.
     
  4. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,674
    That's unnecessary. This is the implementation of "IsNativeObjectAlive" outside of the editor:

    Code (csharp):
    1. bool IsNativeObjectAlive(UnityEngine.Object o) => o.GetCachedPtr() != IntPtr.Zero;
     
    TheZombieKiller likes this.
  5. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    I see, so it's a limitation that only exists in the editor currently (perhaps unintentionally?). That's useful information that I hadn't noticed previously, thanks.
     
  6. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,674
    It's intentional, for supporting various asset manipulation cases. I can't come up with any example off the top of my head, but there are cases where native object is destroyed but "not really" in the editor and can be revived.
     
    TheZombieKiller likes this.