Search Unity

UWP: Does Update _ever_ run on STA thread?

Discussion in 'Windows' started by guavaman, Jul 5, 2019.

  1. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,633
    Hi,

    I'm the developer of Rewired. I have a user who is reporting an issue in his project that I cannot reproduce. He is using Unity 2018.1.1f1 (also tested on 2018.4.3) and building to IL2CPP on an Xbox One home console in development mode. He is reporting a concurrency::invalid_operation exception being thrown by a call to Task::get in Rewired_UWP.dll, a native UWP C++/CX library when pressing the Xbox/Guide button on his controller and then resuming the application. It only happens when he presses the Xbox/Guide button, the Xbox overlay comes up, and then he closes the overlay screen.

    According to the documentation here, this exception will be thrown if Task::get is called from an STA thread:

    I can confirm this exception should only be thrown on an STA thread by looking at ppltasks.h:

    Code (csharp):
    1. #if (_PPL_TASK_CONTEXT_CONTROL_ENABLED)
    2.             if (_IsNonBlockingThread())
    3.             {
    4.                 // In order to prevent Windows Runtime STA threads from blocking the UI, calling task.wait() task.get() is illegal
    5.                 // if task has not been completed.
    6.                 if (!_IsCompleted() && !_IsCanceled())
    7.                 {
    8. #ifdef BUILD_WINDOWS
    9.                     _THROW(invalid_operation, "Illegal to wait on a task in a Windows Runtime STA");
    10. #else
    11.                     _THROW(invalid_operation("Illegal to wait on a task in a Windows Runtime STA"));
    12. #endif
    13.                 }
    14.                 else
    15.                 {
    16.                     // Task Continuations are 'scheduled' *inside* the chore that is executing on the ancestor's task group. If a continuation
    17.                     // needs to be marshalled to a different apartment instead of scheduling, we make a synchronous cross-apartment COM
    18.                     // call to execute the continuation. If it then happens to do something which waits on the ancestor (say it calls .get(),
    19.                     // which task based continuations are wont to do), waiting on the task group results in waiting on the chore that is making
    20.                     // this synchronous callback, which causes a deadlock. To avoid this, we test the state of the ancestor's event,
    21.                     // and we will NOT wait on it if it has finished execution (which means now we are in the inline synchronous callback).
    22.                     _DoWait = false;
    23.                 }
    24.             }
    25. #endif
    According to Unity documentation, Unity always runs on a separate thread:
    https://docs.unity3d.com/Manual/windowsstore-appcallbacks.html

    Let’s take a closer look at AppCallbacks class. When you create it, Unity creates a new thread called “AppThread”. This is done because there’s a restriction from Microsoft - if your application does not become responsive after 5 seconds you’ll fail to pass WACK (Windows Application Certification). (You can read more here - http://msdn.microsoft.com/en-us/library/windowsphone/develop/hh184840(v=vs.105).aspx) Imagine if your first level is pretty big and takes a significant amount of time to load. Because your application is running on UI thread, the UI will be unresponsive until your level is fully loaded. That’s why Unity always runs your game on different thread.

    I can confirm through my own testing that Update and FixedUpdate run on an MTA thread, not STA, using the following code:

    Code (csharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class UWPCheck : MonoBehaviour {
    6.  
    7.     private string msg;
    8.     private string error;
    9.  
    10.     private void FixedUpdate() {
    11.         Go();
    12.     }
    13.  
    14.     private void Update() {
    15.         Go();
    16.     }
    17.  
    18.     private void Go() {
    19. #if ENABLE_WINMD_SUPPORT
    20.         msg = System.Threading.Thread.CurrentThread.GetApartmentState().ToString();
    21.         if(msg != "MTA") error = "Error: " + msg;
    22. #else
    23.         msg = "UWP check";
    24. #endif
    25.     }
    26.  
    27.     private void OnGUI() {
    28.         GUILayout.Label(msg);
    29.         GUILayout.Label(error);
    30.     }
    31.  
    32. }
    There is never a time I get any result but MTA in my testing.

    Making this issue even stranger, this user is reporting that after loading a saved game in his game, the error stops happening when opening and closing the overlay. Upon restarting the game, the problem returns.

    It seems very far-fetched, but the only thing I can theorize at this point that is somehow, Update (or Awake) is being called on an STA thread at least once upon resuming the application causing Task::get to throw the exception. I fail to see any other way calling Task::get could throw an concurrency::invalid_operation exception.

    Does Unity _ever_ run Update on the UI thread, in this case, when resuming the application? Does it ever run on a thread pool and might this thread pool include an STA thread?

    Rewired never calls this function that is throwing the exception except on Awake, Update, or Fixed Update (depending on user's settings).

    Another remote possibility: Does IL2CPP ever make P/Invoke calls a thread other than the one making the function call, with the possibility of it being called on an STA thread?

    I cannot reproduce this issue, and neither can the user when testing in a new scene. It only happens in his game which likely includes may calls to various parts of the Unity API. My theory is that something his game is doing is triggering Unity to do something to make the Update callback run on the UI thread. Is something like this even possible?
     
    Last edited: Jul 5, 2019
  2. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,680
    That's a very weird issue you're describing indeed.

    To answer your question: no, Update() never runs on STA thread or a threadpool thread. It will always run it on a dedicated thread we create at the startup of the application, and that thread will be MTA. IL2CPP also does not change threads from under you in a P/Invoke call.

    One thing I might suggest is ask your user to enable "break on all C++ exceptions" in Visual Studio Exception Settings, then once this exception happens, have him save the dump file by doing DEBUG -> Save Dump As. He should zip up the dump file together with GameAssembly.pdb and send it to you. You should be able to open it in Visual Studio and see how the code ends up being on an STA thread.
     
  3. guavaman

    guavaman

    Joined:
    Nov 20, 2009
    Posts:
    5,633
    Thanks for the reply, information, and advice. I'll see if I can get the user to send me the dump. Thanks again!