Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Unity specifically, "where" does an await literally return to?

In Unity, say you have a GameObject . So, it could be Lara Croft, Mario, an angered bird, a particular cube, a particular tree, or whatever.

enter image description here

(Recall that Unity is not OO, it's ECS. The Components themselves which you can "attach" to a GameObject may or may not be created in an OO language, but Unity itself is just a list of GameObjects and a frame engine that runs any Components on them each frame. Thus indeed Unity is of course "utterly" single-thread, there's not even a conceptual way to do anything relating to "actual Unity" (the "list of game objects") on another1 thread.)

So say on the cube we have a Component called Test

public class Test: MonoBehaviour {

It does have an Update pseudofunction, so Unity knows we want to run something each frame.

  private void Update() { // this is Test's Update call
     
     Debug.Log(ManagedThreadId); // definitely 101
     if (something) DoSomethingThisParticularFrame();
  }

Let's say the unity thread is "101".

So that Update (and indeed any Update of any frame on any game object) will print 101.

So from time to time, perhaps every few seconds for some reason, we choose to run DoSomethingThisFrame.

Thus, every frame (obviously, on "the" Unity thread ... there is / can only be one thread) Unity runs all the Update calls on the various game objects.

So on one particular frame (let's say the 24th frame of the 819th second of game play) let's say it does run DoSomethingThisParticularFrame for us.

void DoSomethingThisParticularFrame() {

   Debug.Log(ManagedThreadId); // 101 I think
   TrickyBusiness();
}

I assume that will also print 101.

async void TrickyBusiness() {

   Debug.Log("A.. " + ManagedThreadId); // 101 I think
   var aTask = Task.Run(()=>BigCalculation());
   
   Debug.Log("B.. " + ManagedThreadId); // 101 I think
   await aTask;

   Debug.Log("C.. " + ManagedThreadId); // In Unity a mystery??
   ExplodeTank();
}

void BigCalculation() {
   
   Debug.Log("X.. " + ManagedThreadId); // say, 999
   for (i = 1 to a billion) add
}

OK so

  1. I'm pretty sure at A it will print 101. I think.

  2. I guess that at B it will print 101

  3. I believe, but I'm uncertain, at X it will have started another thread for BigCalculation. (Say, 999.) (But maybe that's wrong, who knows.)

My question, what happens, in Unity, at "C" ?

What thread are we on at C, where it (tries to?) explode a tank????

I believe in normal .Net environments, you'd be on another thread at C, say 202.

(For example, consider this excellent answer and notice the first example output "Thread After Await: 12". 12 is different from 29.)

But that's meaningless in Unity -

... how can TrickyBusiness be on "another thread" - what would that mean, that the whole scene is duplicated, or?

Or is it the case that (in Unity especially and only? IDK),

at the point where TrickyBusiness begins, Unity actually puts that (what - a naked instance of the class "Test" ??) on another thread?

In Unity when you use await what does it print at C, or A for that matter?

It would seem that:

If indeed "C" is on a different thread - you simply can't use awaits in that way in Unity, it would be meaningless.


1Obviously some ancillary calculations (eg, rendering, whatever) are done on other cores, but the actual "frame based game engine" is one pure thread. (It's impossible to "access" the main engine frame thread in any way whatsoever: when you are programming, say, a native plugin or some calculation which runs on another thread, all you can do is leave markers and values for the components on the engine frame thread to look at and use when they run each frame.)

like image 256
Fattie Avatar asked Apr 10 '19 12:04

Fattie


People also ask

What is await in unity?

The final component for writing basic asynchronous tasks is await . Using it with a Task tells the program to return to executing the synchronous code that called the async method. This synchronous code is therefore not blocked, and can continue running at least until it requires the returned Task code.

Does await actually wait C#?

await will asynchronously wait until the task completes. This means the current method is "paused" (its state is captured) and the method returns an incomplete task to its caller. Later, when the await expression completes, the remainder of the method is scheduled as a continuation.

What does await mean C#?

The await keyword is used to asynchronously wait for a Task or Task<T> to complete. It pauses the execution of the current method until the asynchronous task that's being awaited completes.

What does await task Run do?

If a predefined method returns a Task , you simply mark the calling method as async and put the await keyword in front of the method call. It's helpful to understand the control flow associated with the await keyword, but that's basically it.


1 Answers

Async as a high level abstraction is not concerned with threads.

On which thread the execution resumes after an await is controlled by System.Threading.SynchronizationContext.Current.

E.g. WindowsFormsSynchronizationContext will make sure the execution that started on the GUI thread will resume on the GUI thread after an await, so if you perform a test in a WinForms application, you will see that ManagedThreadId is the same after an await.

E.g. AspNetSynchronizationContext does not care about preserving threads and will allow the code to resume on any thread.

E.g. ASP.NET Core does not have a synchronization context at all.

Whatever will happen in Unity depends on what it has as its SynchronizationContext.Current. You can examine what it returns.


The above is a "true enough" representation of events, that is, what you can expect from your normal boring everyday async/await code concerned with regular Task<T> functions that return their results in the usual way.

You absolutely can tweak these behaviours:

  • You can waive the context capturing by calling ConfigureAwait(false) with your awaits. Since the context is not captured, everything that comes with the context is lost, including the ability to resume on the original thread (for contexts that are concerned with threads).

  • You can devise async code that purposely switches you between threads even when you are not using ConfigureAwait(false). A good example can be found in Raymond Chen's blog (part 1, part 2) and shows how to explicitly jump on another thread in the middle of a method with

    await ThreadSwitcher.ResumeBackgroundAsync();
    

    and then come back with

    await ThreadSwitcher.ResumeForegroundAsync(Dispatcher);
    
  • Because the entire async/await mechanism is loosely coupled (you can await any object that defines a GetAwaiter() method), you can come up with an object whose GetAwaiter() does whatever you want with current thread/context (in fact, that is exactly what the above bullet item is).

SynchronizationContext.Current does not magically enforce its ways on other people's code: it is the other way round. SynchronizationContext.Current only has effect because the implementation of Task<T> chooses to respect it. You are free to implement a different awaitable that ignores it.

like image 171
GSerg Avatar answered Oct 22 '22 08:10

GSerg