11

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.


1 Obviously 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.)

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 1
    Nope. C would be 101.. aTask would have been on say 102. but where you print C its still 101, so in your example above A, B and C all print 101. – BugFinder Apr 10 '19 at 13:01
  • @BugFinder - thanks - one thing, if you click to this detailed example, https://stackoverflow.com/a/55604269/294884 the writer says the opposite: notice the first example, output, "29 / 29 /12" ..............??? – Fattie Apr 10 '19 at 13:04
  • 2
    [Async is not about threads](https://stackoverflow.com/q/17661428/11683), but in general, if the code is executed outside of the GUI thread situation (e.g. WinForms), it [may](https://stackoverflow.com/a/17661783/11683) resume on other threads. – GSerg Apr 10 '19 at 13:04
  • @GSerg - ah - are you saying that .Net somehow **knows** if it is a gui-like situation? Unity is, indeed, exactly such a situation. in the example above, it would be totally meaningless/impossible for C to be on another thread. Would this mean that, from the Unity developers point of view, in some way you "flag" .Net letting it know that this is a gui-like situation .............. is that right???? – Fattie Apr 10 '19 at 13:07
  • 1
    Like Stephen Cleary [explains in the comments](https://stackoverflow.com/questions/17661428/async-stay-on-the-current-thread/17661783#comment25724009_17661783), async only cares about the synchronization context. E.g. WinForms provides a context that makes sure execution goes back to the GUI thread, e.g. ASP.NET provides a context that does not care about threads. I don't know what Unity provides, but async will act accordingly to that. – GSerg Apr 10 '19 at 13:09
  • Ahhhhh !!! @GSerg So the behavior of async totally depends on the "context". All of that is hidden from you / just doesn't come up when working in Unity. I'm just wondering, is there a way to "find out" about the context you are "in"? BTW surely you should put in an answer to claim the coming huge bounty. – Fattie Apr 10 '19 at 13:12
  • 1
    @Fattie I coded up an example: Here was the result https://imgur.com/5q3sl3I – BugFinder Apr 10 '19 at 13:23
  • 3
    `is there a way to "find out" about the context you are "in"` - well, like the same comment mentions, you do that by examining `SynchronizationContext.Current`, which will give you an instance of [`SynchronizationContext`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext?view=netframework-4.7.2). Can't really put any of that as an answer though because all I said was "Please see this other question". – GSerg Apr 10 '19 at 13:23
  • astonishing, @LeoBartkus ! how do you guys know this stuff ! So indeed Leo, you are sayting that ***it's a fact that In unity specifically, C will indeed come back on 101.*** Because of the technical stuff you mentioned! Is that right??? – Fattie Apr 10 '19 at 13:24
  • @BugFinder - My God ! – Fattie Apr 10 '19 at 13:24
  • 1
    @GSerg - ahhh! unreal. My God man, you can put in an answer! All you have to say is "it depends on the context" and perhaps "as explained here ..". A massive bounty is coming (you can't add them for a few hours) – Fattie Apr 10 '19 at 13:25
  • 2
    @Fattie given I basically took your code, why did you not code up your own example and test? contexts dont usually change thread mid method. – BugFinder Apr 10 '19 at 13:26
  • @BugFinder - actually I tested it extensively :) But only YOU were decent enough to bring results! But tests prove nothing unfortunately, particularly with threading stuff. Anything could be happening :O – Fattie Apr 10 '19 at 13:34
  • If you run enough copies of the test and see the same results.. you would have a pretty definitive answer (I believe einsteins theory of insanity was performing the same test and expecting different answers) – BugFinder Apr 10 '19 at 13:38
  • LOL on the Einstein @BugFinder . Hmm, I know what you mean but that's not really the case with threading. When threading is involved, you'd have to do remarkably exhaustive testing - every platform, every type of load and memory on the devices, etc etc. Different things can happen depending on such factors.. :O – Fattie Apr 10 '19 at 13:45
  • I think you already have your answer, but some of the technical information in the question about OO, ECS and Unity being single-threaded is incorrect. I suggest you remove them or correct them. – Paiman Roointan Dec 14 '19 at 09:22

1 Answers1

16

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.

GSerg
  • 76,472
  • 17
  • 159
  • 346
  • This would have to be one of the best answers on the whole site. *"On which thread the execution resumes after an await is controlled by System.Threading.SynchronizationContext.Current."* I am astonished I did not find / come across that information *anywhere*, in spite of asking many similar questions, etc. Cheers! – Fattie Apr 10 '19 at 13:46
  • 2
    As for the resuming an execution I'd say its' not 100% accurate since `ConfigureAwait` is also a factor which has an influence on this. Apart from that the `async/await` is just an abstraction and you can build your own awaiter/awaitable which would always resume on the original thread or just ignore the synchronization context at all. – Dmytro Mukalov Apr 10 '19 at 14:01
  • @Fattie That's because it's probably too simplified to be always true. – Dmytro Mukalov Apr 10 '19 at 14:06
  • @DmytroMukalov , that sounds like fantastic information. Hopefully you Answer. It does seem to be the case that Unity has a context which results in the "same after" condition ........ – Fattie Apr 10 '19 at 14:10
  • 2
    Well, surely if *you* say "Please ignore context" by calling `ConfigureAwait(false)`, the current context will be ignored, no surprise here. But if the *called code* does it, *you* will [still resume on your context](https://stackoverflow.com/a/27851460/11683). – GSerg Apr 10 '19 at 15:15
  • 1
    For reference: https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs It seems the native Unity runtime calls `ExecuteTasks()` inside the Unity main thread, but so far I haven't found out whether that is within the `Update()` frame part or at some other point. Relevant for physics for example. – AyCe May 16 '22 at 16:32