I was playing around with async tasks in Unity when I noticed some behavior I couldn't explain. While printing out some values on the console to see what was going on, I noticed something about the thread IDs of the various scenarios I was playing around with. The code below includes the following:
- Two tests within a "fire and forget" async void to get everything going
- A task awaiting another task (a small delay)
- And a few debug logs to monitor the thread IDs of each started task
TEST 1 within the Start method just awaits the SimpleTask and gives the following output:
Task started on thread: 1
SimpleTask before delay on thread: 1
SimpleDelay on thread: 1
SimpleTask after delay on thread: 1
TEST 2 on the other hand leads to this:
Task started on thread: 1
SimpleTask before delay on thread: 25
SimpleDelay on thread: 25
SimpleTask after delay on thread: 26
Why does the second test result in the use of other threads? My understanding of this line is that it simply wraps the first test in an extra layer of an async task. I know there's no point in doing that, but I can't figure out why it completely changes the results of the initial test. It's not like the first test is running synchronously. I would like to understand what is going on here, the more explanation the better.
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class TestAsyncTask : MonoBehaviour
{
private async void Start()
{
Debug.Log($"Task started on thread: {Thread.CurrentThread.ManagedThreadId}");
// TEST 1
await SimpleTaskAsync();
// TEST 2
await Task.Factory.StartNew(async () => await SimpleTaskAsync());
}
private async Task SimpleTaskAsync()
{
Debug.Log($"SimpleTask before delay on thread: {Thread.CurrentThread.ManagedThreadId}");
await SimpleDelayAsync();
Debug.Log($"SimpleTask after delay on thread: {Thread.CurrentThread.ManagedThreadId}");
}
private async Task SimpleDelayAsync()
{
Debug.Log($"SimpleDelay on thread: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
}
}
Additional specs (I doubt they matter but here are they anyway):
- Unity version 2021.3.11f1
- API compatibility level: .NET Framework
CodePudding user response:
await
by default will capture the current context (SynchronizationContext
or TaskScheduler
) and will resume executing the async
method in that context; if there is no context, then they will run on a thread pool thread. I recommend you read my async
intro which goes into more detail.
In this case, I assume the code is executed within a UI context (on the UI thread). UI frameworks provide a SynchronizationContext
so that code after the await
resumes on the UI thread.
My understanding of this line is that it simply wraps the first test in an extra layer of an async task.
The reason the second code doesn't use the UI thread is because of the StartNew
(which, as I describe on my blog, is dangerous; use Task.Run
instead). Task.Run
always executes its delegate on a thread pool thread, and StartNew
(in this case) does, too. Thread pool threads do not have any context for await
to capture, so the code after the await
runs on any available thread pool thread.