17

Important for anyone researching this difficult topic in Unity specifically,

be sure to see another question I asked which raised related key issues:

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


For C# experts, Unity is single-threaded1

It's common to do calculations and such on another thread.

When you do something on another thread, you often use async/wait since, uh, all the good C# programmers say that's the easy way to do that!

void TankExplodes() {

    ShowExplosion(); .. ordinary Unity thread
    SoundEffects(); .. ordinary Unity thread
    SendExplosionInfo(); .. it goes to another thread. let's use 'async/wait'
}

using System.Net.WebSockets;
async void SendExplosionInfo() {

    cws = new ClientWebSocket();
    try {
        await cws.ConnectAsync(u, CancellationToken.None);
        ...
        Scene.NewsFromServer("done!"); // class function to go back to main tread
    }
    catch (Exception e) { ... }
}

OK, so when you do this, you do everything "just as you normally do" when you launch a thread in a more conventional way in Unity/C# (so using Thread or whatever or letting a native plugin do it or the OS or whatever the case may be).

Everything works out great.

As a lame Unity programmer who only knows enough C# to get to the end of the day, I have always assumed that the async/await pattern above literally launches another thread.

In fact, does the code above literally launch another thread, or does c#/.Net use some other approach to achieve tasks when you use the natty async/wait pattern?

Maybe it works differently or specifically in the Unity engine from "using C# generally"? (IDK?)

Note that in Unity, whether or not it is a thread drastically affects how you have to handle the next steps. Hence the question.


Issue: I realize there's lots of discussion about "is await a thread", but, (1) I have never seen this discussed / answered in the Unity setting (does it make any difference? IDK?) (2) I simply have never seen a clear answer!


1 Some ancillary calculations (eg, physics etc) are done on other threads, 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 programming, say, a native plugin or some calculation on another thread, you just leave markers and values for the components on the engine frame thread to look at and use when they run each frame.)

Fattie
  • 30,632
  • 54
  • 336
  • 607
  • it depends on the platform and the scripting backend. – 0xBFE1A8 Mar 29 '19 at 18:03
  • 1
    @0xBFE1A8 ah I see. Fascinating! – Fattie Mar 29 '19 at 19:03
  • 1
    On iOS Unity uses [GCD](https://developer.apple.com/documentation/dispatch?language=objc) library. – 0xBFE1A8 Mar 29 '19 at 19:06
  • hmm do you think mono goes down that deep ? with say Windows builds now it;'s the iL2CPP backend right? – Fattie Mar 29 '19 at 19:12
  • https://docs.unity3d.com/Manual/ScriptingRestrictions.html – 0xBFE1A8 Mar 29 '19 at 19:14
  • incredible, @0xBFE1A8 ! that actually tells us what's what on platforms! – Fattie Mar 29 '19 at 19:17
  • Mono is legacy and will be deprecated. On iOS Unity does convert all c# code to C++. – 0xBFE1A8 Mar 29 '19 at 19:18
  • 1
    Therefore Most of the writing about threads in Unity is incorrect! – 0xBFE1A8 Mar 29 '19 at 19:26
  • https://docs.unity3d.com/Manual/IL2CPP-HowItWorks.html – 0xBFE1A8 Mar 29 '19 at 19:27
  • [Do all C++ compilers support the async/await keywords?](https://stackoverflow.com/questions/39106863/do-all-c-compilers-support-the-async-await-keywords) – 0xBFE1A8 Apr 08 '19 at 18:03
  • Hi @Fattie, welcome back to SO! I am also confuse about async operation that it really create another thread or not. Cause I am getting freeze problem whenever i download the asset bundles. Unity says that asset bundle are load in main thread that halt the execution for a while. Then it means that unity is multi thread? – Muhammad Faizan Khan Apr 11 '19 at 10:05
  • https://stackoverflow.com/questions/55570170/huge-world-loading-unloading-in-unity-webgl-using-asset-bundles – Muhammad Faizan Khan Apr 11 '19 at 10:05
  • hi @MuhammadFaizanKhan !! I have been right here old friend :) CRITICAL INFORMATION , note the answer from StevePy below but do note my new question https://docs.unity3d.com/540/Documentation/Manual/DownloadingAssetBundles.html in fact it has more definitive answers *in the Unity case* – Fattie Apr 11 '19 at 11:11
  • pls see this answer: https://stackoverflow.com/a/27089652/366064 – Bizhan Apr 13 '19 at 11:36
  • @Bizhan that's a very important QA, thahks, I'll study it. However in the **specific Unity case** did you see this answer https://stackoverflow.com/a/55614146/294884 about `SynchronizationContext` – Fattie Apr 13 '19 at 11:40
  • @Fattie thx for the link, I don't have the answer to your question, I always used Coroutines in Unity but this link explains why and how to use async instead of coroutines: http://www.stevevermeulen.com/index.php/2017/09/using-async-await-in-unity3d-2017/ – Bizhan Apr 13 '19 at 11:48
  • again thanks a million for that blog link @Bizhan again I will study that - however, on a quick read, I fear that writer may be totally misguided about a number of issues; a lot fo that code would just hang the current frame - based on the information here https://stackoverflow.com/a/55614146/294884 It's a tricky business! – Fattie Apr 13 '19 at 11:58
  • @Fattie, Ah! I see your point, I can't say without proper testing. I guess waiting for the task to finish might **require prolonging the frame** in which the task was run. or the task might end up **finishing in a later frame** which is more probable to me. I'm starring this question to see the final answer :) – Bizhan Apr 13 '19 at 13:33
  • right I want to know too @Bizhan . I'm almost certain it won't finish in a later frame (somewhat like a coroutine) But - IDK !!!!!!!!!!! – Fattie Apr 13 '19 at 15:42

5 Answers5

18

This reading: Tasks are (still) not threads and async is not parallel might help you understand what's going on under the hood. In short in order for your task to run on a separate thread you need to call

Task.Run(()=>{// the work to be done on a separate thread. }); 

Then you can await that task wherever needed.

To answer your question

"In fact, does the code above literally launch another thread, or does c#/.Net use some other approach to achieve tasks when you use the natty async/wait pattern?"

No - it doesn't.

If you did

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

Then cws.ConnectAsync(u, CancellationToken.None) would run on a separate thread.

As an answer to the comment here is the code modified with more explanations:

    async void SendExplosionInfo() {

        cws = new ClientWebSocket();
        try {
            var myConnectTask = Task.Run(()=>cws.ConnectAsync(u, CancellationToken.None));

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // class function to go back to main tread
        }
        catch (Exception e) { ... }
    }

You might not need it on a separate thread though because the async work you're doing is not CPU bound (or so it seems). Thus you should be fine with

 try {
            var myConnectTask =cws.ConnectAsync(u, CancellationToken.None);

            // more code running...
await myConnectTask; // here's where it will actually stop to wait for the completion of your task. 
            Scene.NewsFromServer("done!"); // continue from here
        }
        catch (Exception e) { ... }
    }

Sequentially it will do exactly the same thing as the code above but on the same thread. It will allow the code after "ConnectAsync" to execute and will only stop to wait for the completion of "ConnectAsync" where it says await and since "ConnectAsync" is not CPU bound you (making it somewhat parallel in a sense of the work being done somewhere else i. e. networking) will have enough juice to run your tasks on, unless your code in "...." also requires a lot of CPU bound work, that you'd rather run in parallel.

Also you might want to avoid using async void for it's there only for top level functions. Try using async Task in your method signature. You can read more on this here.

agfc
  • 832
  • 6
  • 13
  • Wait - look at your last sentence. Does it mean: , "... would run on a separate thread. And then *when that is finished* the next line of code (so, the "...") would then run." Am I right ??! – Fattie Mar 29 '19 at 13:16
  • This is true but i dont recommend using this because if ya use many threads all you gonna see is performance drops because of Unitys `UnitySynchronizationContext` its better to use jobs to be honest async and await is meh currently(for separate threads) – Menyus Mar 29 '19 at 14:02
  • @Fattie If you await it right there, then yes, it will wait for that thread to finish and the task to complete. If you want things to run in parallel then you should assign that task to a variable and then await it wherever you want. I'll put more info in my answer but you should be able to get that info from the link I posted. – agfc Mar 29 '19 at 15:04
  • @Menyus , a good point. One thing though, performance is rarely the issue: when doing Unity programming it's (absolutely) essential to know whether you're still on the Unity thread or not. – Fattie Mar 29 '19 at 19:15
  • @agfc - wait !!!!!!!!!!!!!! in this amazing system. In you first code example where it uses Task.Run to specifically make a new thread. Eventually it "comes back" at the "await myConnectTask". In fact, does it know to "come back" ***ON THE ORIGINAL THREAD*** ? If so - holy crap, that is clever !!!!!!!!! – Fattie Apr 08 '19 at 11:12
  • @Fattie, that is one of the use cases `async/await` was created for. You can opt-out from returning to the original thread by specifying `ConfigureAwait(false)`. However, all this works for a .NET runtime. I have no idea what happens when the managed code is translated into native by Unity's IL2CPP. – dymanoid Apr 08 '19 at 14:41
  • @dymanoid - amazing! that's actually really really really cool. Bravo MSFT or whoever is responsible. That's my favorite metaphor for threads/tasks I've seen. thanks for the explanation. Ah - but does that mean the whole function "async void SendExplosionInfo ..." in fact starts off on another thread? In the unity context if some component (ie, running once a frame) happens to call `SendExplosionInfo` ... it's not like that component can "wait" (the whole overall frame would wait) until the return. Or perhaps it does, and in that example its the unity programmer's responsibility... – Fattie Apr 08 '19 at 16:24
  • ... to not do that. I guess I can ask a new question about that, but no Unity programmer will know. :) I'll try some investigation. {PS just as you say the cws.ConnectAsync is a bad example since it goes away anyways.} – Fattie Apr 08 '19 at 16:25
  • @dymanoid , not a problem, I was just wondering why you deleted your amazing answer?? – Fattie Apr 10 '19 at 11:41
5

No, async/await does not mean - another thread. It can start another thread but it doesn't have to.

Here you can find quite interesting post about it: https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

Adam Jachocki
  • 1,494
  • 8
  • 19
  • Ah ,thanks - that's from an actual MSFT guy right. Damn, I have to read that more carefully - it's still confusing! – Fattie Mar 29 '19 at 13:14
  • 2
    It will *never* start another thread. `await` doesn't execute anything by itself, it awaits already executing asynchronous operations without blocking the current thread – Panagiotis Kanavos Mar 29 '19 at 13:14
  • 1
    @Fattie it's not confusing - `await` awaits, it doesn't start anything. Adding `async` to a method signature won't make it asynchronously by magic. – Panagiotis Kanavos Mar 29 '19 at 13:16
  • @PanagiotisKanavos - fantastic, got it. You should pick up Unity, you'd make a fortune by actually understanding c# :) – Fattie Mar 29 '19 at 13:17
  • @Fattie once it clicks in your head you can produce pretty efficient code by properly using a combination of async and parallel code. – agfc Mar 29 '19 at 15:22
5

Important notice

First of all, there's an issue with your question's first statement.

Unity is single-threaded

Unity is not single-threaded; in fact, Unity is a multi-threaded environment. Why? Just go to the official Unity web page and read there:

High-performance multithreaded system: Fully utilize the multicore processors available today (and tomorrow), without heavy programming. Our new foundation for enabling high-performance is made up of three sub-systems: the C# Job System, which gives you a safe and easy sandbox for writing parallel code; the Entity Component System (ECS), a model for writing high-performance code by default, and the Burst Compiler, which produces highly-optimized native code.

The Unity 3D engine uses a .NET Runtime called "Mono" which is multi-threaded by its nature. For some platforms, the managed code will be transformed into native code, so there will be no .NET Runtime. But the code itself will be multi-threaded anyway.

So please, don't state misleading and technically incorrect facts.

What you're arguing with, is simply a statement that there is a main thread in Unity which processes the core workload in a frame-based way. This is true. But it isn't something new and unique! E.g. a WPF application running on .NET Framework (or .NET Core starting with 3.0) has a main thread too (often called the UI thread), and the workload is processed on that thread in a frame-based way using the WPF Dispatcher (dispatcher queue, operations, frames etc.) But all this doesn't make the environment single-threaded! It's just a way to handle the application's logic.


An answer to your question

Please note: my answer only applies to such Unity instances that run a .NET Runtime environment (Mono). For those instances that convert the managed C# code into native C++ code and build/run native binaries, my answer is most probably at least inaccurate.

You write:

When you do something on another thread, you often use async/wait since, uh, all the good C# programmers say that's the easy way to do that!

The async and await keywords in C# are just a way to use the TAP (Task-Asynchronous Pattern).

The TAP is used for arbitrary asynchronous operations. Generally speaking, there is no thread. I strongly recommend to read this Stephen Cleary's article called "There is no thread". (Stephen Cleary is a renowned asynchronous programming guru if you don't know.)

The primary cause for using the async/await feature is an asynchronous operation. You use async/await not because "you do something on another thread", but because you have an asynchronous operation you have to wait for. Whether there is a background thread this operation will run or or not - this does not matter for you (well, almost; see below). The TAP is an abstraction level that hides these details.

In fact, does the code above literally launch another thread, or does c#/.Net use some other approach to achieve tasks when you use the natty async/wait pattern?

The correct answer is: it depends.

  • if ClientWebSocket.ConnectAsync throws an argument validation exception right away (e.g. an ArgumentNullException when uri is null), no new thread will be started
  • if the code in that method completes very quickly, the result of the method will be available synchronously, no new thread will be started
  • if the implementation of the ClientWebSocket.ConnectAsync method is a pure asynchronous operation with no threads involved, your calling method will be "suspended" (due to await) - so no new thread will be started
  • if the method implementation involves threads and the current TaskScheduler is able to schedule this work item on a running thread pool thread, no new thread will be started; instead, the work item will be queued on an already running thread pool thread
  • if all thread pool threads are already busy, the runtime might spawn new threads depending on its configuration and current system state, so yes - a new thread might be started and the work item will be queued on that new thread

You see, this is pretty much complex. But that's exactly the reason why the TAP pattern and the async/await keyword pair were introduced into C#. These are usually the things a developer doesn't want to bother with, so let's hide this stuff in the runtime/framework.

@agfc states a not quite correct thing:

"This won't run the method on a background thread"

await cws.ConnectAsync(u, CancellationToken.None);

"But this will"

await Task.Run(()=> cws.ConnectAsync(u, CancellationToken.None));

If ConnectAsync's synchronous part implementation is tiny, the task scheduler might run that part synchronously in both cases. So these both snippets might be exactly the same depending on the called method implementation.

Note that the ConnectAsync has an Async suffix and returns a Task. This is a convention-based information that the method is truly asynchronous. In such cases, you should always prefer await MethodAsync() over await Task.Run(() => MethodAsync()).

Further interesting reading:

dymanoid
  • 13,597
  • 3
  • 34
  • 59
  • @Fattie, I undeleted my answer. I previously deleted it because it only applies to those Unity instances that run a .NET Runtime. I have no generic answer for all Unity variants, including those transforming the C# code into native. – dymanoid Apr 10 '19 at 13:45
3

I don't like answering my own question, but as it turns out none of the answers here is totally correct. (However many/all of the answers here are hugely useful in different ways).

In fact, the actual answer can be stated in a nutshell:

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

That's it.

Thus in any particular version of Unity (and note that, as of writing 2019, they are drastically changing Unity - https://unity.com/dots) - or indeed any C#/.Net environment at all - the question on this page can be answered properly.

The full information emerged at this follow-up QA:

https://stackoverflow.com/a/55614146/294884

Community
  • 1
  • 1
Fattie
  • 30,632
  • 54
  • 336
  • 607
1

The code after an await will continue on another threadpool thread. This can have consequences when dealing with non-thread-safe references in a method, such as a Unity, EF's DbContext and many other classes, including your own custom code.

Take the following example:

    [Test]
    public async Task TestAsync()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread Before Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = await names;
            Console.WriteLine("Thread After Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

The output:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 29
Thread Before Await: 29
Thread After Await: 12

1 passed, 0 failed, 0 skipped, took 3.45 seconds (NUnit 3.10.1).

Note that code before and after the ToListAsync is running on the same thread. So prior to awaiting any of the results we can continue processing, though the results of the async operation will not be available, just the Task that is created. (which can be aborted, awaited, etc.) Once we put an await in, the code following will be effectively split off as a continuation, and will/can come back on a different thread.

This applies when awaiting for an async operation in-line:

    [Test]
    public async Task TestAsync2()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = await context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async/Await: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Output:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async/Await: 6
Thread After Async/Await: 33

1 passed, 0 failed, 0 skipped, took 4.38 seconds (NUnit 3.10.1).

Again, the code after the await is executed on another thread from the original.

If you want to ensure that the code calling async code remains on the same thread then you need to use the Result on the Task to block the thread until the async task completes:

    [Test]
    public void TestAsync3()
    {
        using (var context = new TestDbContext())
        {
            Console.WriteLine("Thread Before Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var names = context.Customers.Select(x => x.Name).ToListAsync();
            Console.WriteLine("Thread After Async: " + Thread.CurrentThread.ManagedThreadId.ToString());
            var result = names.Result;
            Console.WriteLine("Thread After Result: " + Thread.CurrentThread.ManagedThreadId.ToString());
        }
    }

Output:

------ Test started: Assembly: EFTest.dll ------

Thread Before Async: 20
Thread After Async: 20
Thread After Result: 20

1 passed, 0 failed, 0 skipped, took 4.16 seconds (NUnit 3.10.1).

So as far as Unity, EF, etc. goes, you should be cautious about using async liberally where these classes are not thread safe. For instance the following code may lead to unexpected behaviour:

        using (var context = new TestDbContext())
        {
            var ids = await context.Customers.Select(x => x.CustomerId).ToListAsync();
            foreach (var id in ids)
            {
                var orders = await context.Orders.Where(x => x.CustomerId == id).ToListAsync();
                // do stuff with orders.
            }
        }

As far as the code goes this looks fine, but DbContext is not thread-safe and the single DbContext reference will be running on a different thread when it is queried for Orders based on the await on the initial Customer load.

Use async when there is a significant benefit to it over synchronous calls, and you're sure the continuation will access only thread-safe code.

Steve Py
  • 15,253
  • 1
  • 18
  • 29
  • Thanks again - FTR in fact I have written a related question! https://stackoverflow.com/questions/55613081/in-unity-specifically-where-does-an-await-literally-return-to – Fattie Apr 10 '19 at 12:55
  • 2
    You answer is not correct. `The code after an await will continue on another threadpool thread` - this is wrong. In fact, the continuation depends on that thread's `SynchronizationContext` where the code before `await` runs. E.g. in a WinForms or a WPF app, the code after `await` will continue (by default) on the same thread where the code before `await` was executed. In your example it is not the case, because you have no `SynchronizationContext`. But Unity **has** a `SynchronizationContext` on its main thread, so code after `await` will continue on that thread, not on the thread pool thread. – dymanoid Apr 10 '19 at 13:55
  • So it does, that was news to me. It effectively means that it is doing the same as example #3 behind the scenes, though if you are writing your own async methods you still need to be aware that they are executing in a separate thread, just with a sync context the resumption would come back to the original thread. (I.e. keep async methods functional) – Steve Py Apr 10 '19 at 22:15
  • Thanks! It's good to see that it sounds like the behaviour is under control. – Steve Py Apr 25 '19 at 21:39