Unity Main Thread Dispatcher & Guard¶
UnityMainThreadDispatcher and UnityMainThreadGuard provide thread-safe access to Unity's main thread from background workers, ensuring callbacks execute correctly and preventing common threading errors.
Overview¶
| Component | Purpose | Usage Pattern |
|---|---|---|
UnityMainThreadDispatcher | Dispatch work to the main thread | dispatcher.RunOnMainThread(() => ...) |
UnityMainThreadGuard | Assert code is on the main thread | UnityMainThreadGuard.EnsureMainThread() |
UnityMainThreadDispatcher¶
The UnityMainThreadDispatcher is the package-wide bridge for marshaling callbacks from worker threads back onto Unity's main thread. The dispatcher is implemented as a RuntimeSingleton<UnityMainThreadDispatcher>, is marked [ExecuteAlways], and runs both in Edit Mode and Play Mode so background logging, importers, and build scripts can all enqueue work safely.
Basic Usage¶
API¶
Use Cases¶
UnityMainThreadGuard¶
The UnityMainThreadGuard is a guard/assertion that throws an exception if called from a background thread. Use it to protect Unity API access points that must never be called off-thread.
⚠️ Important:
EnsureMainThread()does NOT dispatch work to the main thread. It throwsInvalidOperationExceptionif called from a background thread. To dispatch work, useUnityMainThreadDispatcher.Instance.RunOnMainThread().📦 Internal API:
UnityMainThreadGuardis aninternalclass, accessible only within the Unity Helpers assembly or via[InternalsVisibleTo]. For most use cases, prefer usingUnityMainThreadDispatcherdirectly which ispublic.
Basic Usage¶
Why It Exists¶
Problem: Unity APIs must be called from the main thread, but bugs can allow background thread access that causes silent corruption or crashes.
Solution: UnityMainThreadGuard.EnsureMainThread() fails fast with a clear error message, catching threading bugs during development.
API¶
| C# | |
|---|---|
Default Bootstrapping Flow¶
- A
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)](EnsureDispatcherBootstrap) spins up the dispatcher before user assemblies receive their ownRuntimeInitializeOnLoadMethodcallbacks. This guarantees that worker threads can enqueue diagnostics as soon as your game loads. - Inside the editor,
[InitializeOnLoadMethod](EnsureDispatcherBootstrapInEditor) mirrors the same behavior for Edit Mode tools. The bootstrap politely skips when play mode is already running, so it does not duplicate the instance scene-side. - Both entry points run through
UnityMainThreadGuard.EnsureMainThread(...)before touching Unity APIs, so the dispatcher is always created on the real main thread even when background code tries to force instantiation.
With no configuration, the dispatcher therefore always exists, enforces the queue bound exposed via PendingActionLimit, and is hidden from the hierarchy during Edit Mode by HideFlags.HideInHierarchy | HideFlags.HideInInspector | HideFlags.NotEditable.
Opting In/Out of Auto-Creation¶
Occasionally (especially in tests) you want to control exactly when the dispatcher is created so that scenes unload cleanly and Unity does not warn about hidden objects surviving domain reloads. Use the internal switch:
| C# | |
|---|---|
- The toggle is global to the current domain; disabling it prevents the runtime/editor bootstrap hooks from creating fresh GameObjects.
- When you re-enable auto-creation the very next call to
UnityMainThreadDispatcher.Instancewill recreate the singleton on the main thread (and Play Mode bootstrap will recreate it on the following domain reload). - Always destroy the existing dispatcher GameObject (
UnityMainThreadDispatcher.DestroyExistingDispatcheror the test helper) before turning the feature back on so that stale instances are removed fromResources.FindObjectsOfTypeAll.
AutoCreationScope (scoped toggles)¶
UnityMainThreadDispatcher.AutoCreationScope wraps the toggle/cleanup pattern above in an IDisposable so you cannot forget to restore the previous state:
- On enter, the scope captures the previous
AutoCreationEnabledvalue, switches to the desired state, and optionally destroys any existing dispatcher instances. - On dispose, the scope restores the original toggle and (when requested) destroys any dispatcher that may have been created during the scoped work, ensuring follow-up tests inherit a clean slate.
Test Bootstrap Workflow¶
Two dedicated bootstraps live under Tests/*/TestUtils and keep dispatcher lifetimes predictable during automated suites:
Runtime / PlayMode (Tests/Runtime/TestUtils/UnityMainThreadDispatcherTestBootstrap.cs)¶
| C# | |
|---|---|
- Runs before every play-mode domain reload.
- Forces any hidden dispatcher left over from a prior scene to be
DestroyImmediate'd so play-mode tests can opt into dispatcher usage explicitly. - Guarantees a clean slate for suites that create and destroy scenes repeatedly (see
CommonTestBase.BaseSetUp).
Editor / EditMode (Tests/Editor/TestUtils/UnityMainThreadDispatcherEditorTestBootstrap.cs)¶
| C# | |
|---|---|
- Executes once per editor domain reload (before EditMode tests run).
- Ensures the hidden dispatcher object is removed from the hierarchy so Unity's test runner does not flag leaked objects between assemblies.
Re-enabling per Test¶
The runtime and editor CommonTestBase fixtures demonstrate the intended per-test lifecycle via UnityMainThreadDispatcher.AutoCreationScope:
- At
[SetUp]it grabsUnityMainThreadDispatcher.CreateTestScope(destroyImmediate: true)which internally disables auto-creation, destroys stragglers, and then re-enables auto-creation so the test can accessInstancenormally. - Production code can create/destroy the dispatcher freely; the scope tracks everything automatically.
- During every teardown stage it disposes the scope, restoring the previous auto-creation flag and destroying any dispatcher created while the test runs — no manual try/finally blocks required.
Downstream packages can copy the exact pattern:
- Add a
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]that disables auto-creation and destroys lingering instances. - Add an
[InitializeOnLoad]editor bootstrap that mirrors the same behavior for Edit Mode. - When a single test needs to temporarily disable the dispatcher, wrap the custom logic in
using var scope = UnityMainThreadDispatcher.AutoCreationScope.Disabled(...)so cleanup always runs.
Following this workflow keeps Unity from warning about hidden GameObjects during test teardown, prevents worker threads from instantiating the dispatcher off-thread, and documents exactly when auto-creation is in effect for your package.