Use Unity API from Another Thread or Call a Function in the Main Thread

Call methods from the main thread - UnityEngine C#

This Loom class is able to call the specific method from the Main thread, this is how you do It:

public class Loom : MonoBehaviour
{
public static int maxThreads = 10;
static int numThreads;

private static Loom _current;
private int _count;
public static Loom Current
{
get
{
Initialize();
return _current;
}
}

public void Awake()
{
_current = this;
initialized = true;
}

static bool initialized;

static void Initialize()
{
if (!initialized)
{

if (!Application.isPlaying)
return;
initialized = true;
var g = new GameObject("Loom");
_current = g.AddComponent<Loom>();
}

}

private List<Action> _actions = new List<Action>();
public struct DelayedQueueItem
{
public float time;
public Action action;
}
private List<DelayedQueueItem> _delayed = new List<DelayedQueueItem>();

List<DelayedQueueItem> _currentDelayed = new List<DelayedQueueItem>();

public static void QueueOnMainThread(Action action)
{
QueueOnMainThread(action, 0f);
}
public static void QueueOnMainThread(Action action, float time)
{
if (time != 0)
{
lock (Current._delayed)
{
Current._delayed.Add(new DelayedQueueItem { time = Time.time + time, action = action });
}
}
else
{
lock (Current._actions)
{
Current._actions.Add(action);
}
}
}

public static Thread RunAsync(Action a)
{
Initialize();
while (numThreads >= maxThreads)
{
Thread.Sleep(1);
}
Interlocked.Increment(ref numThreads);
ThreadPool.QueueUserWorkItem(RunAction, a);
return null;
}

private static void RunAction(object action)
{
try
{
((Action)action)();
}
catch
{
}
finally
{
Interlocked.Decrement(ref numThreads);
}

}


public void OnDisable()
{
if (_current == this)
{

_current = null;
}
}



// Use this for initialization
public void Start()
{

}

List<Action> _currentActions = new List<Action>();

// Update is called once per frame
public void Update()
{
lock (_actions)
{
_currentActions.Clear();
_currentActions.AddRange(_actions);
_actions.Clear();
}
foreach (var a in _currentActions)
{
a();
}
lock (_delayed)
{
_currentDelayed.Clear();
_currentDelayed.AddRange(_delayed.Where(d => d.time <= Time.time));
foreach (var item in _currentDelayed)
_delayed.Remove(item);
}
foreach (var delayed in _currentDelayed)
{
delayed.action();
}
}
}



//Usage
public void Call()
{
if (Thread.CurrentThread.ManagedThreadId != TestClass.MainThread.ManagedThreadId)
{
Loom.QueueOnMainThread(() => {
Call();
});
return;
}
Console.WriteLine("Hello");
}

Invoke method in main thread from other thread

To Skip this i implement a queue system for my messages from server.

Invoking a function in main thread via background thread in Unity

There are multiple ways probably.

The most commonly used is a so called "Main Thread Dispatcher" pattern which might look like e.g.

// An object used to LOCK for thread safe accesses
private readonly object _lock = new object();
// Here we will add actions from the background thread
// that will be "delayed" until the next Update call => Unity main thread
private readonly Queue<Action> _mainThreadActions = new Queue<Action>();

private void Update ()
{
// Lock for thread safe access
lock(_lock)
{
// Run all queued actions in order and remove them from the queue
while(_mainThreadActions.Count > 0)
{
var action = _mainThreadActions.Dequeue();

action?.Invoke();
}
}
}

void retrieveData()
{
while (true)
{
var data = subSocket.ReceiveFrameString();


// Any additional parsing etc that can still be done on the background thread

// Lock for thread safe access
lock(_lock)
{
// Add an action that requires the main thread
_mainThreadActions.Enqueue(() =>
{
// Something that needs to be done on the main thread e.g.
var text = new GameObject("Info").AddComponent<Text>();
text.text = Data;
}
}
}
}

Starting Coroutine in Main Thread

Most of the Unity API can only be used on the Unity main-thread, StartCoroutine is one of those things.

or another solution to my issue

If you already are on a background thread you can also simply use UnityWebRequest.Get without any Coroutine and just wait using e.g.

request.SendWebRequest(); 
while(!request.isDone)
{
// depends on your use case, but in general for the love of your CPU
// you do want to wait a specific amount that is "ok" as a delay
// you could of curse reduce this to approximately frame wise check by using e.g. 17 ms (=> checks 60 times per second)
Thread.Sleep(50); // checks 20 times per second
}

if (request.isNetworkError)
{
Debug.Log("Error While Sending: " + request.error);
}
else
{
Debug.Log("Received: " + request.result);
}

Have in mind though that your are still on a background thread and also other Unity API calls might not be allowed at this point.


If your method is not on a background thread in general note that StartCoroutine does NOT delay the method calling it (this would be exactly what we want to avoid by using a Coroutine) but rather immediately continues with the rest of your code.

You can of course do something like

var request = UnityWebRequest.Get(url);
request.SendWebRequest();
return request;

without yielding it and outside of any Coroutine but then it is up to you to ensure that request.isDone is true before you continue accessing and using the results.



How I can enforce a Coroutine to run on the main-thread?

The alternative in order to force things happening on the main thread you can use a pattern often called Main thread dispatcher. A simplified version could look like e.g.

public class MainThreadDispatcher : MonoBehaviour
{
#region Singleton pattern (optional)
private static MainThreadDispatcher _instance;
public static MainThreadDispatcher Instance => _instance;

private void Awake()
{
if(_instance && _instance != this)
{
Destroy(gameObject);
return;
}

_instance = this;
DontDestroyOnLoad(gameObject);
}
#endregion Singleton

#region Dispatcher
// You use a thread-safe collection first-in first-out so you can pass on callbacks between the threads
private readonly ConcurrentQueue<Action> mainThreadActions = new ConcurrentQueue<Action>();

// This now can be called from any thread/task etc
// => dispatched action will be executed in the next Unity Update call
public void DoInMainThread(Action action);
{
mainThreadActions.Enqueue(action);
}

// In the Unity main thread Update call routine you work off whatever has been enqueued since the last frame
private void Update()
{
while(mainThreadActions.TryDequeue(out var action))
{
action?.Invoke();
}
}
#endregion Dispatcher
}

and then in your case you could use

//MainThreadDispatcher.Instance.DoInMainThread(() =>
yourMainThreadDispatcherReference.DoInMainThread(() =>
{
StartCoroutine(GetRequest(url));
});

SetActive() can only be called from the main thread

So my answer is very similar to the accepted answer from Milod's, but a little different, as it took me a while to wrap my head around his, even though his still works.

  1. The Issue:
    Normally, all your code runs on a single thread in Unity, since Unity is single-threaded,
    however when working with APIs like Firebase, which require callbacks, the callback functions will be handled by a new thread.
    This can lead to race-conditions, especially on a single-threaded engine like Unity.

  2. The solution (from Unity):
    Starting from Unity 2017.X, unity now requires changes to UI components to be run on the Main thread (i.e. the first thread that was started with Unity).

  3. What is impacted ?:
    Mainly calls that modify the UI like...

    gameObject.SetActive(true);  // (or false)
    textObject.Text = "some string" // (from UnityEngine.UI)
  4. How this relates to your code:

public void SignInWithEmail() {
// auth.SignInWithEmailAndPasswordAsyn() is run on the local thread,
// ...so no issues here
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {

// .ContinueWith() is an asynchronous call
// ...to the lambda function defined within the task=> { }
// and most importantly, it will be run on a different thread, hence the issue
DatabaseReference.GetValueAsync().ContinueWith(task => {

//HERE IS THE PROBLEM
userPanel.SetActive(true);
authPanel.SetActive(false);
}
}
}


  1. Suggested Solution:
    For those calls which require callback functions, like...
DatabaseReference.GetValueAsync()

...you can...

  • send them to a function which is set up to run on that initial thread.
  • ...and which uses a queue to ensure that they will be run in the order that they were added.
  • ...and using the singleton pattern, in the way advised by the Unity team.

Actual solution

  1. Place the code below into your scene on a gameObject that will always be enabled, so that you have a worker that...

    • always runs on the local thread
    • can be sent those callback functions to be run on the local thread.
using System;
using System.Collections.Generic;
using UnityEngine;

internal class UnityMainThread : MonoBehaviour
{
internal static UnityMainThread wkr;
Queue<Action> jobs = new Queue<Action>();

void Awake() {
wkr = this;
}

void Update() {
while (jobs.Count > 0)
jobs.Dequeue().Invoke();
}

internal void AddJob(Action newJob) {
jobs.Enqueue(newJob);
}
}


  1. Now from your code, you can simply call...

     UnityMainThread.wkr.AddJob();

...so that your code remains easy to read (and manage), as shown below...

public void SignInWithEmail() {
auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {

DatabaseReference.GetValueAsync().ContinueWith(task => {
UnityMainThread.wkr.AddJob(() => {
// Will run on main thread, hence issue is solved
userPanel.SetActive(true);
authPanel.SetActive(false);
})

}
}
}

Framerate lag spikes from network requests on main thread

I'd say that probably the lag spikes are coming from the JSON parsing and transforming it into an AudioClip. You can probably do this on a different thread:

Task<byte[]> ParseJsonData (string rawJson)
{
try
{
return Task.Run(() =>
{
jsonData = JSON.Parse(rawJson);
string stringData = jsonData["audioContent"]["data"].ToString();
return AudioHelpers.ConvertToByteStream(stringData);
});
}
catch (Exception e)
{
UnityEngine.Debug.LogException(e);
throw;
}
}

And call it like this:

IEnumerator getResponse(Conversation conversation)
{
WWWForm form = new WWWForm();
form.AddField("id", this.id);
var www = UnityWebRequest.Post("http://" + Datastore.Instance.host + ":3000/generate", form);

yield return www.SendWebRequest();

if (interrupted) yield break;

if (www.isNetworkError)
{
Debug.Log(www.error);
}
else
{
if (www.GetResponseHeaders().Count > 0)
{
ParseJsonData(rawJson)
.ContinueWith(ParseAudioData);
ParseAudioData(www.downloadHandler.text).ContinueWith((rawData) =>
{
// https://github.com/PimDeWitte/UnityMainThreadDispatcher
UnityMainThreadDispatcher.Instance.Enqueue(() =>
{
AudioClip clip = AudioHelpers.ConvertToAudioClip(rawData);
StartCoroutine(PlayDialog(clip));
Debug.Log("Response Recieved");
});
}
}
}

IEnumerator PlayDialog (AudioClip clip)
{
this.audioSource.clip = clip;
this.audioSource.Play();
this.animator.SetBool(this.talkingBoolHash, true);
yield return new WaitForSeconds(clip.length);
if (interrupted) yield break;
this.conversation.currentSpeaker = jsonData["nextSpeaker"].ToString().Replace("\"", "");
this.conversation.processing = false;
this.animator.SetBool(this.talkingBoolHash, false);
}

This will send the operations to a new thread and continue with your code when the operations end. If the lag spikes are coming from parsing the json this should help it. Take care when running code from other threads, since you cannot access most of Unity functionality and it's all async.

Edit: Included some utils for running code on the MainThread, because as others pointed out, this wouldn't work without fiddling with synchronization context.

Also, I'd recommend for you to try to separate your methods with single responsibilities. As of now your coroutine is downloading stuff, parsing it, playing audio and updating the dialog status. This is quite a lot a for a single function.

Your JSON should also be a structured class, it makes little sense to parse it and still access stuff by its string hash.

If this still doesn't solve your issue, you might need to dive a bit deeper into the profiler and check exactly what is causing it spend so much time in the main thread.



Related Topics



Leave a reply



Submit