Is `Task` in Unity C# the exact same as in .NET?

Is the Task that you can use in Unity scripts, the exact same as in .NET? I’m asking, because I’m curious if I can use .NET documentation on Task for using Task in Unity. Docs like: Asynchronous programming - C# | Microsoft Learn

Unity docs say that:

Unity supports a simplified asynchronous programming model using the .NET async key word and await operator.

(here: Unity - Manual: Asynchronous programming with the Awaitable class)

And Unity is using a custom version of Mono. So I’m guessing that they have simplified the implementation of Task? And I can’t rely on .NET docs for using Task in Unity?

Yes it is the exact same Task, for this reason you should be careful when using it because it doesn’t have any knowledge of Unity’s game loop, so for example if you start an asynchronous operation when you have pressed play and then you exit play mode the task will keep running, unless you cancel it manually.

The Awaitable is Unity’s implementation of a Task equivalent that has knowledge of Unity’s game loop and respects it while also offering methods that interact with it. It also has other differences, for example it is pooled so it can be awaited only once.

2 Likes

System.Threading.Tasks.Task works fine in Unity with caveats mentioned above.

Though it’s a bit heavy-weight for Unity. I’d suggest to use Unity’s Awaitables, or UniTask: GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

2 Likes

I worked on a turn based game “prototype” which used async code for the “turn loop”, everything was using Tasks instead of Awaitable or UniTask, because I already knew about Tasks from a C# dev day job (how exceptions and cancellation behave etc. so no surprises there).

I didn’t notice any issues, everything “just worked”, the alternatives are maybe more lightweight, but as always, this depends on your game context and you can use the profiler to check if normal Tasks cause any performance issues in your specific project.

I will continue to use them as a default and only replace them in cases where I notice issues, btw you can also mix Tasks and Awaitables, if you need some Unity specific things from the Awaitable API like await for the next frame.

Btw Unity uses a custom SynchronizationContext to support async await, you can read about it if you care about the details, but it basically assures that the code after await runs on the “main” thread, similar how the SynchronizationContext of WinForms or WPF works.
https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs

I’ve been under the false impression that having async methods return Awaitable instead of Task would make Unity automatically cancel them when exiting Play Mode, but this does not actually seem to be the case.

It doesn’t appear to matter in the slightest if your methods return Task or Awaitable, but the only thing that matters is that you use the static methods in the Awaitable class instead of the ones in the Task class when suspending executing of your async methods.

So just always use Awaitable.WaitForSecondsAsync instead of Task.Delay, and Awaitable.EndOfFrameAsync/NextFrameAsync instead of Task.Yield, and your suspended async methods will automatically not resume executing after you’ve exited Play Mode if they were waiting for any of those methods to complete.

Test code:

using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

public class AsyncMethodExitPlayModeTest : MonoBehaviour
{
	// Async methods stop when exiting play mode if this is set to true:
	[SerializeField] bool useAwaitableStaticMethods = false; 
	
	// This doesn't make any difference:
	[SerializeField] bool returnAwaitable = false;

	async void Start()
	{
		if(returnAwaitable)
		{
			await MethodThatReturnsAwaitable();
		}
		else
		{
			await MethodThatReturnsTask();
		}
	}

	private async Task MethodThatReturnsTask()
	{
		for(int i = 1; i <= 10; i++)
		{
			if(useAwaitableStaticMethods)
			{
				await Awaitable.WaitForSecondsAsync(0.5f);

				Debug.Log($"await using Awaitable.WaitForSecondsAsync ({i} / {10})...");
			}
			else
			{
				for(int y = 0; y < 10; y++)
				{
					await Task.Yield();
				}
				
				Debug.Log($"await using Task.Yield ({i} / {10})...");
			}

			if(i == 5)
			{
				Debug.Log("Exiting play mode...");
				EditorApplication.ExitPlaymode();
			}
		}
		
		Debug.Log("MethodThatReturnsTask completed.");
	}
	
	private async Awaitable MethodThatReturnsAwaitable()
	{
		for(int i = 1; i <= 10; i++)
		{
			if(useAwaitableStaticMethods)
			{
				await Awaitable.WaitForSecondsAsync(0.5f);

				Debug.Log($"await using Awaitable.WaitForSecondsAsync ({i} / {10})...");
			}
			else
			{
				for(int y = 0; y < 10; y++)
				{
					await Task.Yield();
				}
				
				Debug.Log($"await using Task.Yield ({i} / {10})...");
			}
			
			if(i == 5)
			{
				Debug.Log("Exiting play mode...");
				EditorApplication.ExitPlaymode();
			}
		}
		
		Debug.Log("MethodThatReturnsAwaitable completed.");
	}
}

So it seems that the only reason why one might want to make an async method return Awaitable over a Task would be to reduce the amount of garbage being generated, since the prior uses object pooling behind the scenes.

1 Like