Most Unity Developers might be aware of the concept of Asynchronous Programming - if “Coroutines” are what came into your mind, yes, that is what I was hinting at. While Unity provides Coroutines to achieve asynchronous programming, I thought it would be a good idea to share my knowledge about how asynchronous programming can be achieved using core C# concepts - “Tasks” and “Async and Await”. Read on to know more about how these two concepts can be used to achieve asynchronous programming, their similarities and differences.
Why Task, Async/Await when we have Coroutines ?
Coroutines cannot return values. This can lead programmers to introduce extra variables to hold the result.
You cannot start a coroutine in class which doesn't inherit Mohobehaviour. This is a super obvious problem which Unity Developers encounter.
Let me come back to Asynchronous programming first!!
Any program code usually runs straight along, that is, line by line.
For example, let's take a simple game which is run by 4 functions - Function1(), Function2(),Function3() and Function4().
.
.
Function1();
Function2();
Function3();
Function4();
.
.
As you can understand from the above image, Function1() gets executed first and performs its operations, then Function2(), 3 and 4 are executed. Pretty straight forward !
Now let me introduce to you a use case where Function2() performs some operations that take 3 seconds to complete. When the game is run, since Function2() is taking 3 seconds to complete, the game freezes for 3 seconds. This is because the controller stays at Function2() and waits for that to get completed. So every time Function2() gets executed, the game would stop responding until the said function returns back the controller. This is not just a case with games, this could happen in any application you develop.
So, how do we handle this use case now?
Ideally, we would want Function2() to run in the background and not freeze the game.
The new flow should be something like - Function1() -> Function2() -- Result awaited!! -> Function3() -> Function4() -> Function2() result is returned after 3 seconds!!
This is what asynchronous programming does.
Asynchronous operation means that a process operates independently of other processes, whereas synchronous operation means that the process runs only as a result of some other process being completed or handed off.
In asynchronous operations, you can move to another task before the previous one finishes.
C# provides us with “Tasks” to handle use cases such as the one we discussed above.
A Task class represents a single operation that does not return a value and that usually executes asynchronously. The work performed by a Task object typically executes asynchronously on a thread pool thread rather than synchronously on the main application thread .
To create asynchronous execution flow, you need to understand Task Class.
Ok, let me take a simple function SomeTimeConsumingFunction(), which takes 5 seconds to complete its execution.
private void SomeTimeConsumingFunction()
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
}
Thread.Sleep(5000) will pause the controller for 5 seconds!!
Now lets execute this code in Start().
void Start()
{
Debug.Log("Execution Flow 1");
SomeTimeConsumingFunction();
Debug.Log("Execution Flow 2");
Debug.Log("Execution Flow 3");
}
Output:
Execution Flow 1
Task Completed.
Execution Flow 2
Execution Flow 3
when you execute this in Unity, you can observe Unity prints "Execution Flow 1" and freezes for 5 seconds and then prints "Task Completed", "Execution Flow 2" and "Execution Flow 3" on the console.
Of Course I don't want to freeze my game for 5 seconds, because of one function which is taking 5 seconds to complete its execution. I want this function to execute on a separate thread asynchronously and return back when the execution is completed. This behavior can be achieved using Task.
All i have to do is create a Task and Start the task. The work performed by Task object typically executes asynchronously on a thread pool.
To create a task object, an Action is expected in its constructor. So, we can pass SomeTimeConsumingFunction as the action in the constructor of the task object created.
Task task = new Task(SomeTimeConsumingFunction);
private void SomeTimeConsumingFunction()
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
}
Now that we have created task object, we can start this task using task.Start().
void Start()
{
Debug.Log("Execution Flow 1");
Task task = new Task(SomeTimeConsumingFunction);
task.Start();
Debug.Log("Execution Flow 2");
Debug.Log("Execution Flow 3");
}
When the above code is executed, Unity won't freeze our game !
The output is:
Execution Flow 1
Execution Flow 2
Execution Flow 3
Task Completed.
Here, the flow executes normally. First, Execution flow 1,2,3 is printed to the console and after 5 seconds, "Task Completed" is printed. If we compare this output with the previous one, "Task Completed" output was in between "Execution Flow 1" and "Execution Flow 2". There, the controller waited for SomeTimeConsumingFunction() to complete its execution and froze our game but now, using Task, we were able to execute the SomeTimeConsumingFunction() function separately without freezing our game and return back safely when the execution is completed.
2 separate tasks can also be run asynchronously at the same time(parallelly).
void Start()
{
Debug.Log("Execution Flow 1");
Task task1 = new Task(SomeTimeConsumingFunction1);
task1.Start();
Task task2 = new Task(SomeTimeConsumingFunction2);
task2.Start();
Debug.Log("Execution Flow 2");
Debug.Log("Execution Flow 3");
}
private void SomeTimeConsumingFunction1()
{
Thread.Sleep(5000);
Debug.Log("Task1 Completed!!");
}
private void SomeTimeConsumingFunction2()
{
Thread.Sleep(10000);
Debug.Log("Task2 Completed!!");
}
Output:
Execution Flow 1
Execution Flow 2
Execution Flow 3
Task Completed1!! ----------- Returned after 5 seconds
Task Completed2!! ----------- Returned after 10 seconds
Once way to create and start a task is:
Task task1 = new Task(SomeTimeConsumingFunction1);
task1.Start();
Instead of these 2 lines we can create and start a task like:
Task task1 = Task.Factory.StartNew(SomeTimeConsumingFunction1);
And the third way to achieve this is using Task.Run()
Task task1 = Task.Run(SomeTimeConsumingFunction1);
ASYNC AND AWAIT:
As mentioned earlier, asynchronous method execution can also be achieved using “async and await”.
Before moving on to these keywords, lets understand some basics of Task class.
let us create a simple task called:
public Task SomeTimeConsumingMethod_UsingContinueWith()
{
Task t = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
});
return t;
}
Once you call this Task returned method, it starts executing asynchronously.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Threading.Tasks;
using System.Threading;
public class AsyncProgramming : MonoBehaviour
{
void Start()
{
Debug.Log("Flow Continue1");
SomeTimeConsumingMethod_UsingContinueWith();
Debug.Log("Flow Continue2");
Debug.Log("Flow Continue3");
Debug.Log("Flow Continue4");
}
public Task SomeTimeConsumingMethod_UsingContinueWith()
{
Task t = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
});
return t;
}
public void FunctionToPerformAsSoonAsTaskCompletes()
{
Debug.Log("This is a Something to perform on Task Completion!!");
}
}
Output:
Flow Continue1
Flow Continue2
Flow Continue3
Flow Continue4
Task Completed1!! ----------- Returned after 5 seconds
Now let's say you want to perform some actions as soon as the task completes. You can use "ContinueWith" which is a function in the Task class.
SomeTimeConsumingMethod_UsingContinueWith().ContinueWith((task) => { if(task.IsCompleted) FunctionToPerformAsSoonAsTaskCompletes(); });
ContinueWith() returns task as an output, along with which it also gives information on task output statuses such using the fields isComplete or isfaulted or isCanceled.
ContinueWith() function on Task class might look pretty straight forward, but the way it works is a bit different.
Let us take an example to understand this.
In unity, I want to disable a gameobject after 3 seconds using task.
Code:
public class ContinueWith : MonoBehaviour
{
[SerializeField] GameObject toDisable;
void Start()
{
Task task2 = Task.Run(SomeTimeConsumingFunction).ContinueWith((t)
=>
{
Debug.Log("Object Disables!!");
toDisable.SetActive(false);
});
}
private void SomeTimeConsumingFunction()
{
Thread.Sleep(2000);
Debug.Log("Task2 Completed!!");
}
}
Try this simple code on your Unity Editor.
What would you expect to happen? The object should get disabled after the task completes(2 seconds). But this does not happen. Why do you think so? Yes, you do get to see the Debug.log("Object Disables!!") statement printed on the screen , but the object will not be disabled.
Why?
ContinueWith code snippet and the object deactivation code snippet run on different threads. ContinueWith runs on the default thread scheduler which doesn't have any information about the synchronization context to run the object deactivation code.
So to avoid this, we have to make sure both run on same thread.
So TaskScheduler.FromCurrentSynchronizationContext() does the job from us.
Code:
Task task2 = Task.Run(SomeTimeConsumingFunction).ContinueWith((t) => { Debug.Log("Object Disables!!"); toDisable.SetActive(false); },TaskScheduler.FromCurrentSynchronizationContext());
TaskScheduler.FromCurrentSynchronizationContext() makes sure that the ContinueWith code runs on the current thread in which the deactivation code snippet is also being run, instead of on the default thread. Hence, now the object gets disabled after 2 seconds
Modified code to disable object after 2 seconds-
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class ContinueWith : MonoBehaviour
{
[SerializeField] GameObject toDisable;
void Start()
{
Task task2 = Task.Run(SomeTimeConsumingFunction).ContinueWith((t)
=>
{
Debug.Log("Object Disables!!");
toDisable.SetActive(false);
},TaskScheduler.FromCurrentSynchronizationContext());
}
private void SomeTimeConsumingFunction()
{
Thread.Sleep(2000);
Debug.Log("Task2 Completed!!");
}
}
Now lets come back to ASYNC AND AWAIT-
Before discussing what async and await keywords are for, remember that async and await go together. A task method has to be tagged async to use await keyword in it.
The job of async and await is the same as the ContinueWith() function. Await will pause the flow at that particular line and return back once the task execution is completed.
Let’s try to understand how async and await work through this simple code.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Threading.Tasks;
using System.Threading;
public class AsyncProgramming : MonoBehaviour
{
void Start()
{
Debug.Log("Flow Continue1");
SomeTimeConsumingMethod_UsingAsync();
Debug.Log("Flow Continue2");
Debug.Log("Flow Continue3");
Debug.Log("Flow Continue4");
}
public async Task SomeTimeConsumingMethod_UsingAsync()
{
Task t = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
});
await t;
FunctionToPerformAsSoonAsTaskCompletes();
}
public void FunctionToPerformAsSoonAsTaskCompletes()
{
Debug.Log("This is a Something to perform on Task Completion!!");
}
}
You can see that the Task returned function SomeTimeConsumingMethod_UsingAsync() has an async keyword attached to it. If you observe the body of the function, it has "await t" which is waiting for task "t" to get completed. Once the task t is completed, FunctionToPerformAsSoonAsTaskCompletes() is executed.
Output:
Flow Continue1
Flow Continue2
Flow Continue3
Flow Continue4
After 5 seconds--
Task Completed!!
This is a Something to perform on Task Completion!!
A key point to note here is, await can only be used on async Functions.
So the functionality of await is partially same as ContinueWith().
What ever comes after await get executed after the task it awaited ,completes.
In the below code, I wrote two same functionalities, which execute in the same way - one using await and the other using continueWith().
public class AsyncProgramming : MonoBehaviour
{
void Start()
{
Debug.Log("Flow Continue1");
SomeTimeConsumingMethod_UsingAsync();
SomeTimeConsumingMethod_UsingContinueWith().ContinueWith((task) => { if(task.IsCompleted) FunctionToPerformAsSoonAsTaskCompletes(); });
Debug.Log("Flow Continue2");
Debug.Log("Flow Continue3");
Debug.Log("Flow Continue4");
}
public async Task SomeTimeConsumingMethod_UsingAsync()
{
Task t = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
});
await t;
FunctionToPerformAsSoonAsTaskCompletes();
}
public Task SomeTimeConsumingMethod_UsingContinueWith()
{
Task t = Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
Debug.Log("Task Completed!!");
});
return t;
}
public void FunctionToPerformAsSoonAsTaskCompletes()
{
Debug.Log("This is a Something to perform on Task Completion!!");
}
}
SomeTimeConsumingMethod_UsingAsync() and SomeTimeConsumingMethod_UsingContinueWith() gives you the same output in the above code.
Although both async, await and ContinueWith() give us the same output, there are some differences in how these operate under the hood. I would like to discuss one key difference.
1. Using await ,the current state/context is saved and once the awaited task completes its execution, the flow picks up the saved context and starts to execute the statement after await. (Note: State/context is having details about execution context/synchronization context.).
Recollecting from the example I discussed above, where we tried deactivating the object using continueWith() by passing TaskScheduler.FromCurrentSynchronizationContext()). This extra code to set the context to the current thread is not required when using await keyword, because the execution flow picks up the saved state data after await unlike continueWith() which doesn't save any kind of state(because it runs on the default thread scheduler in case a scheduler is not provided). Unity has provided a default SynchronizationContext called UnitySynchronizationContext for async/await which automatically collects any async code that is queued each frame and continues running them on the main unity thread(Although this doesn't happen in C#, this special functionality is provided by unity)
You could explore more differences by experimenting with these two methods and decide which one would fit the best. In the end, it all comes down to personal choice.
I want to conclude this blog by showing how can we achieve the same result as Coroutines using Async/await/Task -
Lets take a simple example which is pretty much self explanatory,
Using Coroutines:
private IEnumerator AsyncFunction_UsingCoroutines()
{
yield return new WaitForSeconds(4f);
Debug.Log("Completed!!");
}
Using Task/Async/Await
private async void AsyncFuction_UsingTask()
{
await Task.Delay(System.TimeSpan.FromSeconds(4));
Debug.Log("Completed!!");
}
Both does the same job when called in start - that is wait for 4 seconds and print "Completed!!".
Complete code:
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
public class CoroutinesAndAsync : MonoBehaviour
{
void Start()
{
Debug.Log("Function 1");
Debug.Log("Function 2");
//Using Coroutines
StartCoroutine(AsyncFunction_UsingCoroutines());
//Using ASync/Await/Task
AsyncFuction_UsingTask();
Debug.Log("Function 3");
Debug.Log("Function 4");
}
//Using Coroutines
private IEnumerator AsyncFunction_UsingCoroutines()
{
yield return new WaitForSeconds(4f);
Debug.Log("Completed!!");
}
private async void AsyncFuction_UsingTask()
{
await Task.Delay(System.TimeSpan.FromSeconds(4));
Debug.Log("Completed!!");
}
}
Output:
Function 1
Function 2
Function 3
Function 4
Completed!! ---- After 4 seconds
Comments