In Unity / C#, does .Net's async/await start, literally, another thread?

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.


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/


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:

  • await vs await Task.Run
  • return Task.Run vs await Task.Run

Tags:

C#

.Net

Unity3D