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.
(Recall that Unity is not OO, it's ECS. The Component
s 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 GameObject
s and a frame engine that runs any Component
s 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
I'm pretty sure at A it will print 101. I think.
I guess that at B it will print 101
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.)
What thread are we on at C, where it (tries to?) explode a tank????
(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?
await
what does it print at C, or A for that matter?It would seem that:
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.)
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With