Class ThreadUtil
Provides static cross-thread synchronization utilities for Unity 6.3+ (.NET 8). Covers volatile reads and writes, memory barriers, cooperative thread yielding, atomic interlocked operations, and safe dispatch of work back to the Unity main thread.
Inherited Members
Namespace: Scylla.Core.Util
Assembly: ScyllaCore.dll
Syntax
public static class ThreadUtil
Remarks
Main thread identity: The class self-initializes via
RuntimeInitializeOnLoadMethod before the first scene loads, capturing the
managed thread ID and SynchronizationContext of the Unity main thread.
All subsequent calls to IsMainThread and RunOnMainThread(Action)
rely on this captured state.
Main thread dispatch: RunOnMainThread(Action) prefers posting via the
captured SynchronizationContext (the standard Unity path). If the context
is unavailable, actions are enqueued in Scylla.Core.Util.ThreadUtil._mainThreadQueue and must be
drained manually by calling ProcessMainThreadQueue() from a
MonoBehaviour.Update().
Thread safety: All members of this class are thread-safe. The static fields are written once during initialization and read-only thereafter, except for Scylla.Core.Util.ThreadUtil._mainThreadQueue which uses a ConcurrentQueue<T>.
Properties
IsMainThread
Gets a value indicating whether the currently executing thread is the Unity main thread. Determined by comparing the calling thread's ManagedThreadId against the ID captured during initialization. This is safe to call from any thread.
Declaration
public static bool IsMainThread { get; }
Property Value
| Type | Description |
|---|---|
| bool |
|
Methods
Add(ref int, int)
Atomically adds value to location and returns
the original value of location before the addition. This
differs from Increment(ref int) in that the return value is the pre-addition
snapshot, not the post-addition result.
Declaration
public static int Add(ref int location, int value)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
| int | value | The amount to add. May be negative to perform atomic subtraction. |
Returns
| Type | Description |
|---|---|
| int | The value of |
See Also
CompareExchange(ref int, int, int)
Atomically compares the current value of location to
comparand and, only if they are equal, replaces it with
value. Returns the original value of
location regardless of whether the exchange occurred.
Declaration
public static int CompareExchange(ref int location, int value, int comparand)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
| int | value | The new value to write if the comparison succeeds. |
| int | comparand | The value to compare against the current value of |
Returns
| Type | Description |
|---|---|
| int | The value of |
Remarks
This is the fundamental primitive for implementing lock-free data structures and state machines. A common pattern is to loop until the compare-exchange succeeds:
int original, updated;
do
{
original = ThreadUtil.VolatileRead(ref _state);
updated = ComputeNextState(original);
}
while (ThreadUtil.CompareExchange(ref _state, updated, original) != original);
See Also
Decrement(ref int)
Atomically decrements location by one and returns the resulting
value. The decrement and the read of the new value are performed as a single
indivisible operation, safe for shared counters accessed by multiple threads.
Declaration
public static int Decrement(ref int location)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
Returns
| Type | Description |
|---|---|
| int | The value of |
See Also
Exchange(ref int, int)
Atomically replaces the value of location with value
and returns the original value. The exchange is guaranteed to be indivisible:
no other thread can observe a state between the read of the old value and the write of
the new value.
Declaration
public static int Exchange(ref int location, int value)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
| int | value | The new value to store in |
Returns
| Type | Description |
|---|---|
| int | The value of |
See Also
Increment(ref int)
Atomically increments location by one and returns the resulting
value. The increment and the read of the new value are performed as a single
indivisible operation, making this safe for shared counters accessed by multiple threads
without acquiring a lock.
Declaration
public static int Increment(ref int location)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
Returns
| Type | Description |
|---|---|
| int | The value of |
See Also
MemoryBarrier()
Inserts a full memory barrier (fence), preventing the CPU and compiler from reordering memory operations across the barrier boundary. Reads and writes issued before the barrier are guaranteed to complete before any reads or writes issued after it.
Declaration
public static void MemoryBarrier()
Remarks
Delegates to MemoryBarrier(). Use this in lock-free algorithms where you need to ensure that a shared flag or pointer written on one thread is visible to another thread in the correct order, but the overhead of a full lock is undesirable.
ProcessMainThreadQueue()
Drains and executes all actions currently enqueued in the internal main-thread fallback
queue. This method is only needed when the SynchronizationContext was
unavailable at initialization time and actions were queued via the fallback path of
RunOnMainThread(Action). Call this from MonoBehaviour.Update() to
process any pending background-thread work.
Declaration
public static void ProcessMainThreadQueue()
Remarks
Exceptions thrown by individual actions are caught, logged via LogException(Exception), and do not prevent subsequent queued actions from running.
In the normal Unity runtime where a valid SynchronizationContext is captured, this queue is never populated and this method is a no-op.
See Also
RunOnMainThread(Action)
Schedules action to run on the Unity main thread. If the calling
thread is already the main thread, the action is executed synchronously and inline.
Otherwise the action is posted via the captured SynchronizationContext
or, when the context is unavailable, enqueued in an internal
ConcurrentQueue<T> for later processing by ProcessMainThreadQueue().
Declaration
public static void RunOnMainThread(Action action)
Parameters
| Type | Name | Description |
|---|---|---|
| Action | action | The delegate to execute on the main thread. If |
Remarks
Use this method to update Unity objects (e.g., transforms, UI elements, materials)
from background threads or async continuations, since most Unity API
calls are main-thread-only.
When posting via SynchronizationContext, the action is executed at the next Unity frame update. When falling back to the queue, the action will not run until ProcessMainThreadQueue() is called from a MonoBehaviour.
See Also
Sleep(int)
Suspends the current thread for at least milliseconds milliseconds.
The actual suspension may be longer depending on the OS scheduler resolution, which
is typically 10-15 ms on Windows.
Declaration
public static void Sleep(int milliseconds)
Parameters
| Type | Name | Description |
|---|---|---|
| int | milliseconds | The number of milliseconds to suspend the thread. A value of |
Remarks
Never call this from the Unity main thread - doing so freezes rendering and input for the duration of the sleep. This method is intended exclusively for background worker threads that need to throttle their execution rate.
SpinUntil(Func<bool>, int)
Spins (busy-waits) until condition returns true or the
optional timeout elapses. The spin automatically applies an adaptive back-off strategy
(alternating between CPU-level spinning and cooperative yielding) to balance latency
and CPU utilization.
Declaration
public static bool SpinUntil(Func<bool> condition, int timeoutMilliseconds = -1)
Parameters
| Type | Name | Description |
|---|---|---|
| Func<bool> | condition | A Func<TResult> returning |
| int | timeoutMilliseconds | The maximum number of milliseconds to spin before returning |
Returns
| Type | Description |
|---|---|
| bool |
|
Remarks
Delegates to SpinUntil(Func<bool>, int).
Prefer this over a manual while loop around SpinWait(int) because
the adaptive back-off reduces cache-line thrashing on heavily contested shared variables.
See Also
SpinWait(int)
Keeps the current thread in a busy-wait spin loop for iterations
CPU cycles. More CPU-efficient than Sleep(int) for very short anticipated
waits (sub-microsecond) because it avoids the overhead of OS context switching.
Declaration
public static void SpinWait(int iterations)
Parameters
| Type | Name | Description |
|---|---|---|
| int | iterations | The number of spin iterations to execute. Each iteration is a single CPU instruction; appropriate values depend on the target platform. On x86/x64, values in the range 10-100 are typical for spinning before escalating to a yield or lock. |
Remarks
Use only when contention is expected to be extremely brief, such as protecting a small counter update. For longer or unpredictable waits, prefer SpinUntil(Func<bool>, int) or Sleep(int).
See Also
VolatileRead(ref bool)
Reads a bool field with acquire semantics, preventing the processor and
compiler from moving subsequent reads or writes before this load. Ensures that the
value reflects all writes committed by any thread prior to the most recent
corresponding volatile write.
Declaration
public static bool VolatileRead(ref bool location)
Parameters
| Type | Name | Description |
|---|---|---|
| bool | location | A reference to the |
Returns
| Type | Description |
|---|---|
| bool | The current value of |
See Also
VolatileRead(ref double)
Reads a double field with acquire semantics. On 32-bit platforms, prevents
the word-tearing hazard that can affect non-volatile reads of 64-bit values.
Declaration
public static double VolatileRead(ref double location)
Parameters
| Type | Name | Description |
|---|---|---|
| double | location | A reference to the |
Returns
| Type | Description |
|---|---|
| double | The current value of |
See Also
VolatileRead(ref int)
Reads an int field with acquire semantics, ensuring the read is not reordered
before preceding writes or hoisted out of a loop by the JIT compiler.
Declaration
public static int VolatileRead(ref int location)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
Returns
| Type | Description |
|---|---|
| int | The current value of |
See Also
VolatileRead(ref long)
Reads a long field with acquire semantics. On 32-bit platforms this prevents
tearing of the 64-bit value that can occur with non-atomic plain reads.
Declaration
public static long VolatileRead(ref long location)
Parameters
| Type | Name | Description |
|---|---|---|
| long | location | A reference to the |
Returns
| Type | Description |
|---|---|
| long | The current value of |
See Also
VolatileRead(ref float)
Reads a float field with acquire semantics, preventing the read from being
reordered or cached by the compiler or processor across a synchronization boundary.
Declaration
public static float VolatileRead(ref float location)
Parameters
| Type | Name | Description |
|---|---|---|
| float | location | A reference to the |
Returns
| Type | Description |
|---|---|
| float | The current value of |
See Also
VolatileRead<T>(ref T)
Reads a reference-type field with acquire semantics, ensuring the reference is observed in the correct order relative to any writes to the object's fields. This prevents the JIT from caching a stale reference in a register across a synchronization point.
Declaration
public static T VolatileRead<T>(ref T location) where T : class
Parameters
| Type | Name | Description |
|---|---|---|
| T | location | A reference to the reference-type field to read. |
Returns
| Type | Description |
|---|---|
| T | The current reference stored in |
Type Parameters
| Name | Description |
|---|---|
| T | The reference type of the field. Must be a class (the |
See Also
VolatileWrite(ref bool, bool)
Writes a bool value to a field with release semantics, ensuring that all
preceding reads and writes are committed before this store becomes visible to other
threads. A paired VolatileRead(ref bool) on another thread that
observes the new value is guaranteed to also observe all writes that preceded this call.
Declaration
public static void VolatileWrite(ref bool location, bool value)
Parameters
| Type | Name | Description |
|---|---|---|
| bool | location | A reference to the |
| bool | value | The value to store. |
See Also
VolatileWrite(ref double, double)
Writes a double value to a field with release semantics. Prevents word-tearing
on 32-bit platforms and ensures the value is not held only in a CPU register cache.
Declaration
public static void VolatileWrite(ref double location, double value)
Parameters
| Type | Name | Description |
|---|---|---|
| double | location | A reference to the |
| double | value | The value to store. |
See Also
VolatileWrite(ref int, int)
Writes an int value to a field with release semantics, preventing subsequent
reads or writes from being moved before this store by the compiler or processor.
Declaration
public static void VolatileWrite(ref int location, int value)
Parameters
| Type | Name | Description |
|---|---|---|
| int | location | A reference to the |
| int | value | The value to store. |
See Also
VolatileWrite(ref long, long)
Writes a long value to a field with release semantics. On 32-bit platforms
this prevents word-tearing that could allow another thread to observe only half
of the 64-bit write.
Declaration
public static void VolatileWrite(ref long location, long value)
Parameters
| Type | Name | Description |
|---|---|---|
| long | location | A reference to the |
| long | value | The value to store. |
See Also
VolatileWrite(ref float, float)
Writes a float value to a field with release semantics, ensuring the value
is flushed to main memory and not held only in a register or cache line.
Declaration
public static void VolatileWrite(ref float location, float value)
Parameters
| Type | Name | Description |
|---|---|---|
| float | location | A reference to the |
| float | value | The value to store. |
See Also
VolatileWrite<T>(ref T, T)
Writes a reference-type value to a field with release semantics, ensuring that all writes to the object's state performed before this call are visible to any thread that subsequently reads the reference via a corresponding VolatileRead<T>(ref T) call.
Declaration
public static void VolatileWrite<T>(ref T location, T value) where T : class
Parameters
| Type | Name | Description |
|---|---|---|
| T | location | A reference to the reference-type field to write. |
| T | value | The reference to store. May be |
Type Parameters
| Name | Description |
|---|---|
| T | The reference type of the field. Must be a class. |
See Also
Yield()
Voluntarily yields the current thread's remaining time slice to the operating system scheduler, allowing other runnable threads to execute. This is a cooperative hint and may have no effect if no other thread is ready to run.
Declaration
public static void Yield()
Remarks
Preferred over Sleep(int) for very short waits inside tight spin loops, because it does not force the thread to sleep for a minimum scheduler quantum. Call periodically inside long-running loops to avoid CPU starvation of other threads. For frequency-controlled yielding, prefer YieldPeriodically(ref int, int).
See Also
YieldPeriodically(ref int, int)
Yields the current thread occasionally during a tight loop to prevent the calling thread
from monopolizing the CPU. Yields once every yieldFrequency iterations
by checking whether the low-order bits of iterationCount are zero after
incrementing. yieldFrequency must be a power of two for the bitmask
test to work correctly.
Declaration
public static void YieldPeriodically(ref int iterationCount, int yieldFrequency = 256)
Parameters
| Type | Name | Description |
|---|---|---|
| int | iterationCount | A reference to the caller's iteration counter. This value is incremented by one on every call; the caller does not need to increment it separately. |
| int | yieldFrequency | How many iterations to allow between yields. Must be a positive power of two (e.g., 64, 128, 256, 512). The default of 256 is appropriate for most workloads. Smaller values yield more often (lower throughput, more cooperative); larger values yield less often (higher throughput, less cooperative). |
Remarks
Typical usage inside a long-running background loop:
var iter = 0;
while (processing)
{
DoWork();
ThreadUtil.YieldPeriodically(ref iter);
}