Search Unity

Question Accessing Unity API from async method

Discussion in 'Scripting' started by lweist3317, Mar 12, 2023.

  1. lweist3317

    lweist3317

    Joined:
    Oct 12, 2017
    Posts:
    34
    Im currently working with a simple web API to get some information during my application.
    As putting Web requests into a coroutine still blocks the Unity main thread, the web API request are in an async function.

    With this Ive run into a problem however; in this example, A and B will print fine; however the TextMeshProUGUI text property is never set and C is never printed.

    Code (CSharp):
    1. public class Example : MonoBehaviour
    2. {
    3.     TextMeshProUGUI tmpUGUI;
    4.     void Start()
    5.     {
    6.         tmpUGUI = FindObjectOfType<TextMeshProUGUI>();
    7.         Task.Run(GetInfo);
    8.     }
    9.  
    10.     async Task GetInfo()
    11.     {
    12.         Debug.Log("A");
    13.  
    14.         // Await information from some Web API; does not use any Unity API
    15.  
    16.         Debug.Log("B");
    17.         tmpUGUI.text = info.ToString();
    18.         Debug.Log("C");
    19.     }
    20. }
    I know you cannot access Unity's API from another thread besides the main thread.
    As far as Im aware however, async does not start a new thread and would run on the main thread in the example.

    Is there some other error here, or can async methods generally not access Unity API either? If so, why?
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,910
    Do any errors print in the console?

    Also if you intend to use async with Unity, you should look into the UniTask package.
     
  3. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    989
    As far as I'm aware, the generated state machine that runs the function executes the remainder of the function on a different thread after an await has occurred.
     
  4. rdjadu

    rdjadu

    Joined:
    May 9, 2022
    Posts:
    116
    Task.Run is throwing you in the threadpool.
     
    lweist3317 and spiney199 like this.
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,910
    Yeah the above is correct, now I think of it.

    It doesn't seem like you actually care about awaiting the method? It should just be a fire-and-forget
    async void
    method.
    Code (CSharp):
    1. public class Example : MonoBehaviour
    2. {
    3.     TextMeshProUGUI tmpUGUI;
    4.  
    5.     private void Start()
    6.     {
    7.         tmpUGUI = FindObjectOfType<TextMeshProUGUI>();
    8.         GetInfo();
    9.     }
    10.  
    11.     private async void GetInfo()
    12.     {
    13.         await // your web request
    14.         tmpUGUI.text = info.ToString();
    15.     }
    16. }
     
  6. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    989
    I'm pretty sure that is still going to throw them into a different thread after the await.
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,910
    Unity has a custom synchronisation context that runs
    await
    and similar calls on the main thread. They more or less operate like co-routines at that point.

    It's not perfect through, so I always recommend using UniTask which does one better by hooking into the
    UnityEngine.PlayLoop
    to give you much more granular control over await calls.
     
    Voxel-Busters, Bunny83 and Sluggy like this.
  8. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,428
  9. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    989
    Good to know. Tricky stuff like this is why I can never *quite* say for sure about things. It falls under that category of 'implementation details'. I guess I could always look at the IL but ain't nobody got time for that!
     
  10. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,910
    Voxel-Busters likes this.
  11. rdjadu

    rdjadu

    Joined:
    May 9, 2022
    Posts:
    116
    That one has a pretty bad bug.

    Code (CSharp):
    1. public void Enqueue(Action action)
    2. {
    3.     Enqueue(ActionWrapper(action));
    4. }
    5.  
    6.     IEnumerator ActionWrapper(Action a)
    7.     {
    8.         a();
    9.         yield return null;
    10.     }
    Basically calls the given action right away (on the originating thread) when it's queued and then queues it (calling it yet again).
     
  12. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    Huh? are you sure you have looked at the right code snippet? The Enqueue method looks like this:

    Code (CSharp):
    1.     public void Enqueue(IEnumerator action) {
    2.         lock (_executionQueue) {
    3.             _executionQueue.Enqueue (() => {
    4.                 StartCoroutine (action);
    5.             });
    6.         }
    7.     }
    Maybe you're interpreting it wrong? The only thing that happens here is that an action is enqueued in the _executionQueue.

    This part:
    Code (CSharp):
    1. () => {
    2.     StartCoroutine (action);
    3. }
    Just creates an anonymous method / delegate / closure. That method is actually stored in the queue. So StartCoroutine will be called when that queued item is dequeued and executed. This will happen later on the main thread.

    This line:
    Code (CSharp):
    1. Enqueue(ActionWrapper(action));
    Also only creates an IEnumerator object wrapper, nothing more. It's kind of pointless to wrap it in an IEnumerator since we only want to call a normal action here. So we could directly queue the action into the internal queue and everything would be fine. Though it does work the way it's implemented here.

    That's a common misconception about coroutines. Calling your generator method (in this case here
    ActionWrapper(action)
    ) does not execute any of your code yet. It's StartCoroutine would would start iterating your coroutine right up to the first yield when called. However StartCoroutine is not called here. It's called when the actual queued action is dequeued in Update.
     
  13. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    I would have implemented the two Enqueue methods the other way round:

    Code (CSharp):
    1.     public void Enqueue(IEnumerator action) {
    2.         Enqueue(() => {
    3.                 StartCoroutine (action);
    4.         });
    5.     }
    6.  
    7.     public void Enqueue(Action action)
    8.     {
    9.         lock (_executionQueue) {
    10.             _executionQueue.Enqueue (action);
    11.         }
    12.     }
    This would make more sense and doesn't require that ActionWrapper that serves no real purpose besides allocating additional memory.
     
  14. lweist3317

    lweist3317

    Joined:
    Oct 12, 2017
    Posts:
    34
    As @rdjadu pointed out, Task.Run was the issue here.

    This fire-and-forget solution works fine here, but there are other methods using the API on which I would need to wait.
    I still would prefer to stay on the main thread (so not using Task.Run) but for some reason calling GetInfo().Wait() blocks the main thread indefinitely.

    Ive tested this without the Unity API access in GetInfo, and it still blocks forever.
    Ive also ran the exact same code outside of Unity where it worked perfectly fine.
    Anyone have a clue why this is?
     
  15. lweist3317

    lweist3317

    Joined:
    Oct 12, 2017
    Posts:
    34
    For clarity's sake here is the current code
    Code (CSharp):
    1. public class Example
    2. {
    3.     void Start()
    4.     {
    5.         Task t = Task.Run(GetInfo);
    6.         t.Wait(); // Works as expected
    7.  
    8.         GetInfo().Wait(); // Blocks indefinitely
    9.     }
    10.  
    11.     async Task GetInfo()
    12.     {
    13.         await // my web api request
    14.     }
    15. }
     
  16. lweist3317

    lweist3317

    Joined:
    Oct 12, 2017
    Posts:
    34
  17. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,910
    Then just
    await
    those methods too?

    I've never used
    Task.Run
    or
    Task.Wait
    . I just
    await
    anything that will let me do so.

    Like I said, Unity has a custom synchronisation context that should keep things on the main thread so long as you don't engage a thread with the C# API.

    And just use UniTask: https://github.com/Cysharp/UniTask
     
  18. lweist3317

    lweist3317

    Joined:
    Oct 12, 2017
    Posts:
    34
    For some reason I assumed you couldnt make Start / Awake async, so I didnt consider using await here, but it works just fine.

    Will look into UniTask, thanks for the help.