Where is the list of thread safe unity calls? Seems very random.
For example the Vector and Mathf classes can be called in a thread but not Random or Time, is there a rule of thumb we can rely on?
Basically the assumption should be that anything Unity engine related and stateful can’t be called outside of the main thread.
By stateful I mean it relies on the state of the engine in someway. Example ‘Random’ is stateful because each subsequent call relies on the previous. And ‘Time’ is stateful in that it depends on at what point in the game you are, including dependencies on if you’re in Update/FixedUpdate.
Where as Mathf is stateless. It’s just a collection of static methods. Actually it’s even moreso than that… it’s a wrapper around System.Math, so it’s not even Unity specific really (well it has a couple extra methods tossed in that aren’t in System.Math…).
Vector on the other hand is technically stateful… but structs are supposed to be simple data structures. Unity won’t complain about accessing them… because it doesn’t really access the engine in any way. It’s just a struct of 3 floats. BUT this doesn’t mean it’s thread safe! It’s actually not… if one thread changes the x value of a Vector while another thread is calculating the magnitude of it… you might get conflicting information… it won’t crash, but you’ll get conflicting info (you can get the magnitude at any point before or after the x is changed… making it technically incorrect).
So yeah… structs are ok to access (like Vector), they won’t complain. These are the only stateful things you can access from an alternate thread… primarily because you have your own copy independent of the unity engine.
But yeah… there’s technically a difference betwen ‘thread safe’ and ‘unity won’t let you access’. There are things NOT thread safe, but you can access in your own thread. Like all collections in System.Collections & System.Collections.Generic. All of them won’t complain… but they are NOT thread safe!
This ‘stateful’ rule of thumb is a general rule of thumb you can apply to threading in general, not just Unity. If something has state (like an object, which is defined as having state), it’s generally not thread safe unless explicitly designed to be. Because if any 2 threads attempt to modify its state at the same time… you’re going to have issues!
This is actually one of the things people praise functional programming for… functional programming principals are based around the idea that its mostly stateless programming, avoiding these problems outright. It’s why it’s called ‘functional programming’, it’s using the concept of algebraic functions in that any given function defined f(…), for any given input/s, you will always get the same output. OOP contradicts this because a function may return something different, regardless of if you pass in the same input/s, because it may rely on internal state.
Almost every property and method in Unity is not thread safe. The list is far wider than it needs to be. For instance, Application.isEditor should be constant, but it will throw.
It’s also not just the thread that’s unsafe. Even if you’re on the main thread, you may get errors for running during serialization logic (like if you stick code in a constructor). Unity enforces this because that code only runs on the main thread sometimes.
Here’s the thread-safe and constructor-safe calls I know:
-
Mathf
-
Nearly all properties/methods you find in structs
-
The exceptions to this are usually pretty obvious, like some of the new ParticleSystem nested structs.
-
Logging: Debug.Log, etc
Thanks for clearing that up, I didn’t know that engine state is what’s verboten.
Is it manipulating a ParticleSystem struct that’s not allowed or getter access in another thread?
It’s really hard to tell what’s safe and what isn’t. There’s no list of all the thread safe calls, but nearly all of the structs seem to be fine. (It’s only the math-related ones like Vector3 that are useful though.)
ParticleSystem is weird. They recent added a bunch of structs that don’t completely act like structs. For instance ParticleSystem.TriggerModule has an enabled property you can set and it messes with the actual ParticleSystem. Probably it’s just wrapping a reference to ParticleSystem itself and is calling into the engine. Calls that touch Unity objects are not thread safe, so that’s not thread safe.
Yep, it is just a wrapper struct:
/// <summary>
///
/// <para>
/// Script interface for the Trigger module.
/// </para>
///
/// </summary>
public struct TriggerModule
{
private ParticleSystem m_ParticleSystem;
/// <summary>
///
/// <para>
/// Enable/disable the Trigger module.
/// </para>
///
/// </summary>
public bool enabled
{
get
{
return ParticleSystem.TriggerModule.GetEnabled(this.m_ParticleSystem);
}
set
{
ParticleSystem.TriggerModule.SetEnabled(this.m_ParticleSystem, value);
}
}
/// <summary>
///
/// <para>
/// Choose what action to perform when particles are inside the trigger volume.
/// </para>
///
/// </summary>
public ParticleSystemOverlapAction inside
{
get
{
return (ParticleSystemOverlapAction) ParticleSystem.TriggerModule.GetInside(this.m_ParticleSystem);
}
set
{
ParticleSystem.TriggerModule.SetInside(this.m_ParticleSystem, (int) value);
}
}
/// <summary>
///
/// <para>
/// Choose what action to perform when particles are outside the trigger volume.
/// </para>
///
/// </summary>
public ParticleSystemOverlapAction outside
{
get
{
return (ParticleSystemOverlapAction) ParticleSystem.TriggerModule.GetOutside(this.m_ParticleSystem);
}
set
{
ParticleSystem.TriggerModule.SetOutside(this.m_ParticleSystem, (int) value);
}
}
/// <summary>
///
/// <para>
/// Choose what action to perform when particles enter the trigger volume.
/// </para>
///
/// </summary>
public ParticleSystemOverlapAction enter
{
get
{
return (ParticleSystemOverlapAction) ParticleSystem.TriggerModule.GetEnter(this.m_ParticleSystem);
}
set
{
ParticleSystem.TriggerModule.SetEnter(this.m_ParticleSystem, (int) value);
}
}
/// <summary>
///
/// <para>
/// Choose what action to perform when particles leave the trigger volume.
/// </para>
///
/// </summary>
public ParticleSystemOverlapAction exit
{
get
{
return (ParticleSystemOverlapAction) ParticleSystem.TriggerModule.GetExit(this.m_ParticleSystem);
}
set
{
ParticleSystem.TriggerModule.SetExit(this.m_ParticleSystem, (int) value);
}
}
/// <summary>
///
/// <para>
/// A multiplier applied to the size of each particle before overlaps are processed.
/// </para>
///
/// </summary>
public float radiusScale
{
get
{
return ParticleSystem.TriggerModule.GetRadiusScale(this.m_ParticleSystem);
}
set
{
ParticleSystem.TriggerModule.SetRadiusScale(this.m_ParticleSystem, value);
}
}
/// <summary>
///
/// <para>
/// The maximum number of collision shapes that can be attached to this particle system trigger.
/// </para>
///
/// </summary>
public int maxColliderCount
{
get
{
return ParticleSystem.TriggerModule.GetMaxColliderCount(this.m_ParticleSystem);
}
}
internal TriggerModule(ParticleSystem particleSystem)
{
this.m_ParticleSystem = particleSystem;
}
/// <summary>
///
/// <para>
/// Set a collision shape associated with this particle system trigger.
/// </para>
///
/// </summary>
/// <param name="index">Which collider to set.</param><param name="collider">The collider to associate with this trigger.</param>
public void SetCollider(int index, Component collider)
{
ParticleSystem.TriggerModule.SetCollider(this.m_ParticleSystem, index, collider);
}
/// <summary>
///
/// <para>
/// Get a collision shape associated with this particle system trigger.
/// </para>
///
/// </summary>
/// <param name="index">Which collider to return.</param>
/// <returns>
///
/// <para>
/// The collider at the given index.
/// </para>
///
/// </returns>
public Component GetCollider(int index)
{
return ParticleSystem.TriggerModule.GetCollider(this.m_ParticleSystem, index);
}
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetEnabled(ParticleSystem system, bool value);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern bool GetEnabled(ParticleSystem system);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetInside(ParticleSystem system, int value);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int GetInside(ParticleSystem system);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetOutside(ParticleSystem system, int value);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int GetOutside(ParticleSystem system);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetEnter(ParticleSystem system, int value);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int GetEnter(ParticleSystem system);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetExit(ParticleSystem system, int value);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int GetExit(ParticleSystem system);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetRadiusScale(ParticleSystem system, float value);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern float GetRadiusScale(ParticleSystem system);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void SetCollider(ParticleSystem system, int index, Component collider);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern Component GetCollider(ParticleSystem system, int index);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int GetMaxColliderCount(ParticleSystem system);
}
They’re probably uncovering it as a struct to reduce gc calls. In recent years Unity has been more aware of create more standardized interfaces that are also aware of the memory issues of the runtime.
As opposed to the older designs where everything was global statics/singletons.