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:
-
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. -
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. -
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:
var spin = new SpinWait();
while (condition)
spin.SpinOnce();
FinishOperation();
This can be fixed by manually executing any pending tasks on the SynchronizationContext inside the loop (which currently requires reflection to call Exec):
var spin = new SpinWait();
while (condition)
{
if (UnityObjectInternal.CurrentThreadIsMainThread())
MainThreadContext.ExecutePendingTasks();
spin.SpinOnce();
}
FinishOperation();
It would also make sense to expose the HasPendingTasks method and the main thread ID.
- 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:
namespace UnityEngine
{
public partial class Object
{
public static bool CurrentThreadIsMainThread { get; }
public static void EnsureRunningOnMainThread();
}
public sealed class UnitySynchronizationContext : SynchronizationContext
{
public static UnitySynchronizationContext GetUnitySynchronizationContext();
public int ManagedThreadId { get; }
public bool HasPendingTasks { get; }
public void ExecutePendingTasks();
}
}