Unity itself has a very strong threading model, basically all C# code is called from a single thread while rendering. Multithreading is heavily abstracted away, for example with the Job System. All Unity functions (especially MonoBehaviours) assume that they are called from the main rendering thread. This is not really unusual, as all GUI applications have a similar concept even in other frameworks.
The difference here is, that unity doesn’t provide a mechanism like run_on_main_thread() { /* code */ }
like other frameworks do.
To help with multithreading, the ThreadingHelper
class has been built to help with accessing unity objects
inside the rest call handler. The ThreadingHelper
provides the following functions
T ExecuteSync<T>(Func<T> action)
- Execute code inside the main/unity thread and return the resultvoid ExecuteAsync(Action action)
- Execute code inside the main/unity thread asynchronouslyvoid ExecuteAsyncCoroutine(Func<IEnumerator> coroutine)
- Execute code inside the main/unity thread as a coroutineThe ThreadingHelper
class uses the RestServer
class to execute this workload in its update function and therefore inside
the main rendering thread.
All considerations on running code inside a MonoBehaviour.Update()
do apply to code executed with ExecuteSync/Async/Coroutine
as well.
The RestServer
MonoBehaviour must be attached to an enabled GameObject, otherwise ExecuteSync/Async/Coroutine
workloads
are not executed.
Each incoming request spawns a new thread which is then handled by an execution of the handler method provided to the
RegisterEndpoint
functions.
Why is this important? While implementing it has to be considered, that requests can overlapp each other while executing. For example, imagine that a rest endpoint triggers an animation over many frames inside unity. If the animation request is triggered again before the animation has finished, the animation can be executed twice at the same time. If the animation code doesn’t expect this, weird results can happen.
The suggestion is, to lock the animation so it can’t be executed again before it finishes. See this code example.
ThreadingHelper
usageExecuteSync
does three things:
The blocking of the two threads is done with C# WaitHandle
mechanism, which should be lightweight on any platform.
The request handler will be blocked only for a specific amount of time, until a TimeoutException
is raised. The default
timeout is 1000ms. You should make sure that the workload can be executed inside this timeframe or increase the timeout by setting
ThreadingHelper.Instance.ThreadingMillisecondsTimeout
to a higher value.
ExecuteSync
is executed in the main rendering thread therefore the workload should be lightweight and be executed inside the time limit of a frame.
Otherwise, both the request handler and the rendering thread will be blocked until the workload has been executed.
Note Any exceptions thrown inside the ExecuteSync
are catched and transported back to the request handler and rethrown.
var position = ThreadingHelper.Instance.ExecuteSync(() => {
return transform.position;
});
ExecuteAsync
is useful to change status on unity objects without interlocking the request handler and the unity rendering thread.
This type of execution can’t return any values from inside Unity and after the ExecuteAsync
method is called, the request
handler immediately continues execution.
ThreadingHelper.Instance.ExecuteAsync(() => {
transform.position += new Vector3(1.0f, 1.0f, 1.0f);
});
Note Due to the asynchronous nature of this method, exceptions raised in the workload are not propagated to the caller of ExecuteAsync
.
If the workload doesn’t handle the exception, the global async exception handler will log the exception into unity.
ExecuteCoroutine
is very similar to the ExecuteAsync
method, as it doesn’t interlock the unity rendering thread and the request handler.
After calling the method the request handler immediately continues execution. ExecuteCoroutine
can be used to have processing
spread over multiple frames. See the unity documentation for deeper information about Coroutines.
void Start() {
server.EndpointCollection.RegisterEndpoint(HttpMethod.GET, "/position", request => {
ThreadingHelper.Instance.ExecuteAsyncCoroutine(Coroutine);
});
}
private IEnumerator Coroutine() {
yield return null;
}