Asynchronous programming in Unity, Using Coroutine and TAP
[中文版本] Background
A couple of weeks ago, Unity has released a new version, 2017.1. In this version, the most anticipated feature, TAP and Async-await support, finally came out as a beta feature, with .NET 4.6 Experimental Equivalent.
This is not a coincidence. With the Microsoft HoloLens and Windows MR platform strategic layout gradually being mature, many of Unity developers began to study how to interact with the UWP API to obtain the necessary support from windows platform.
As a 3D scripting engine, Unity 5.X and earlier provide only limited. NET 2.0-4. x, c # 4 support. To access a comprehensive asynchronous API still requires a lot of adaptive work. If you are unfamiliar with UWP and TAP the underlying implementations, the developer is hard to port TAP into Unity existing Update Check/Coroutine model development process. In the other hand, experienced UWP developers are also eager to XR world,, but are suffering from lacking async-await/TAP , misunderstanding Coroutine asynchronous model. (I was one of the struggled member.)
Now, with the constant maturity of .net standard libs, which is trying to smooth all platform differences. With efforts from Unity, Microsoft and geeks in community , Unity developers and UWP developers are finally able to develop using the Coroutine and TAP at the same time. That is the kind of news makes me laugh out loud in my dream.
While celebrating, we still should realize that this brings new challenges. We update the function is not to all overturn everything to start again, whetheryou are a Unity master developer, you may have projects that need porting to the UWP platform, or you are a novice of unity but a UWP developer, you may want to upgrade your 2D XAML application to Windows MR platform. Or, if you are completely new to both,just want a lot of Unity and UWP code examples find your App technical feasibility. No matter who you are, , you will eventually encounter this problem: how to interact with Unity Coroutine and. Net TAP correctly.
This article aims to provide you with a clear reference, or to be inspired by the tools provided in this article, to provide better productivity for later work and study.
Environment Preparation
Software installation
When I was writing this article, I was using:
o Windows 10 Creator Update 15063
o Unity 2017.1
o Visual Studio 2017.2
As most of the code snippets only dependents on Unity Editor Console printout, you can have use Unity 2017.1 to proceed.
Project Setting Up
The sample project needs to configure your Unity project to start .NET 4.6 and async support. If you are not familiar with this, it is recommended that you follow the steps below to set up your new project.
1. Start Unity
2. Click on the "New" icon, enter the project name "AsyncExample"
3. Click the "Create Project" button
4. Click on the menu "File-> Build Settings"
5. Click "Player Settings" on the bottom.
Look at your "Inspector" window, there will be some property tabs. Unfold "Other Settings" tab.
6. Find the "Configuration" property group. Change the value of "Scripting Runtime Version *" "Stable (.NET 3.5 Equivalent)" to "Experinmental (.NET 4.6 Equivalent) "
7. Unity Editor needs a complete reboot after your modification, you will see following message.
Select "Restart" to restart the Unity.
After restart, at the same location, properties should be
At this point, Unity Editor obtained the ability to execute c# 6.0, some previously unrecognized class library and syntax will be able to be tested and debugged at the Editor Player, without the need for conditional compilation delay to other platforms to run.
8. In the "Project" window, create a new child folder "Scripts" as child folder of "Assets". Create a new C# script "AsyncExample.cs" in "Scripts"
9. In the "Hierarchy" window, create a new, empty object named "ConsoleScripts"
10. Drag the script you have created into "ConsoleScripts" Game Object
Basic codes
Next, we need to prepare for some infrastructure, allows us to create some simple behaviors for our tests.
Logging
Log output code is very simple, it will help us to Unity Console output format information. First of all, we add the following code in the script:
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class AsyncExample : MonoBehaviour
{
Stopwatch stopwatch;
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
}
void LogToTUnityConsole(object content, string flag, [CallerMemberName] string callerName = null)
{
UnityEngine.Debug.Log($"{callerName}:\t{flag} {content}\t at {stopwatch.ElapsedMilliseconds} \t in thread {Thread.CurrentThread.ManagedThreadId}");
}
}
Stopwatch object can help us trace calling time. When the Unity scene enters the running state, the stopwatch will start, we can clearly see a thread at a certain time, in which function to do what.
Next, In order to study how multiple asynchronous tasks directly link and call, we often need to pre-design some tasks, we often need to design a typical template. which can be easy to control and the output sequence. My choice was Count Down Task.
Countdown: Coroutine Version Code: CoroutineCountDown
public IEnumerator CoroutineCountDown(int count, string flag = "")
{
for (int i = count; i >=0 ; i--)
{
LogToTUnityConsole(i, flag);
yield return new WaitForSeconds(1);
}
}
If you are a beginner of Unity, you can see this is a yield function returns a enumerator, by your c# experience. Unity can use IEnumerator returned this by method, which can populate sequence of YieldInstruction objects (WaitForSeconds). This sequence is consumed by dispatcher in the base class, MonoBehavior. In each of the YieldInstruction consumption, when the asynchronous operation is complete, the dispatcher will join execution back to the main thread of Unity and calls the IEnumerator MoveNext() method to get the next YieldInstruction.
Execution: CoroutineCountDown
Execute separately
Put the following code in the Start method as below, we can run game in Editor and get console output
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
StartCoroutine(CoroutineCountDown(3, "BasicCoCall"));
}
You can see, in the logs, execution thread is always Thread1, count down by 1, every 1 second.
Execute in parallel
As this code snippet runs asynchronously, in Unity in this method execution will not affect another operation executed in main thread. It also does not prevent multiple execution..
Like:
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
StartCoroutine(CoroutineCountDown(3, "BasicCoCallA"));<br> StartCoroutine(CoroutineCountDown(3, "BasicCoCallB"));
}
When we called twice in a row, we'll see the two tasks transformed themselves into slices, and weaved in the execution of the main thread.
Execution in linear
If two or more Coroutines require convergence (including other process controls, such as if / switch / loop), an additional parent Coroutine is required to be scheduled in Unity. We can create the following code:
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
StartCoroutine(CoroutineCountDownSeq(3, "BasicCoCallA", "BasicCoCallB"));
}
public IEnumerator CoroutineCountDownSeq(int countDown, params string[] flags)
{
foreach (var flag in flags)
{
yield return StartCoroutine(CoroutineCountDown(3, flag));
}
}
You can see that parent method enumerates a IEnumerator waiting YieldInstructions (this time UnityEngine.Coroutine class) , so dispatcher can schedule YieldInstructions sequences as one, back to top Scheduler.
Results:
Combining code snippet above, we can also implement complex patterns, such as Timer, Event Recognizer, Fork-Join etc.. We are not unfold all of them here.
Countdown: TaskAsync Version Code :TaskAsyncCountDown
public async Task TaskAsyncCountDown(int count, string flag = "")
{
for (int i = count; i >= 0; i--)
{
LogToTUnityConsole(i, flag);
await Task.Delay(1000);
}
}
This version of the code is very similar to the Coroutine version: same CPS transformation, only using await instead of yield return, and using Task instead if Coroutine.
Another way to achieve same is "async void function".
public async void VoidAsyncCountDown(int count, string flag = "")
{
for (int i = count; i >= 0; i--)
{
LogToTUnityConsole(i, flag);
await Task.Delay(1000);
}
}
In community articles and multiple versions of the documents we mentioned, In addition to using the void function as an event handler, "async void functions" are strongly not recommended. Because the functions dose not return anything Task scheduler can access. This code snippet is presented as a negative example here, for your reference. Execution: TaskAsyncCountDown
Execution in main thread scheduling
When we do not need to do anything for this asynchronous task and just start it, the Task object async method returns is relatively meaningless, we can call an async function the in the Start function directly. Although this will be in the Visual Studio with a green wavy line warning, but this is actually a legitimate usage.
In order to avoid the occurrence of such noise, returned could be received by a temporary variable without any operation on it. Like what:
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var _ = TaskAsyncCountDown(3, "BasicAsyncCall");
}
Result just like Coroutine
As you can see, await in the async functions use the default Task choreographer, completed Task will be executed each time the County asked after conversion back to the Unity of the main thread.
If we want to change this behavior, we can modify the TaskAsyncCountDown Function, modify the following code
public async Task TaskAsyncCountDown(int count, string flag = "")
{
for (int i = count; i >= 0; i--)
{
LogToTUnityConsole(i, flag);
await Task.Delay(1000).ConfigureAwait(false);
}
}
Await Scheduler uses the thread pool, and each step of await is no longer be forced to return to game's main thread, which might improve execution speed. In this case operations to some objects that need be operate in main thread (such as Texture Read-write) will fail.CPU intensive computation are recommended to be scheduled in the thread pool, in order to unblock render updates, which can make same effect as reduce workload in Update() method.
Execute in parallel from main thread
Using the Task, multiple operations can start and return to the main thread.
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var a = TaskAsyncCountDown(3, "BasicAsyncCallA");
var b = TaskAsyncCountDown(3, "BasicAsyncCallB");
}
Execute in linear from main thread
Like the Coroutine, async methods can be linked one after another:
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var t = TaskAsyncCountDownSeq(3, "BasicAsyncCallA", "BasicAsyncCallB");
}
public async Task TaskAsyncCountDownSeq(int countDown, params string[] flags)
{
foreach (var flag in flags)
{
await TaskAsyncCountDown(3, flag);
}
}
>Execution results
Execute in thread pooling
If you are careful enough, you will find out that in ConfigureAwait(false) code execution result, thread id value in first line was 1 .
Before the first await keyword, operations will execute on the caller's thread in typical async executions. In extreme cases, if an asynchronous function executes CPU-intensive or long-running synchronous IO operations at the entrance, main thread of Unity will still be blocked. This can be optimized in the following way:
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var _ = Task.Run(()=>TaskAsyncCountDown(3, "BasicAsyncCall"));
}
At this point the function entry is called by the thread pool scheduler. Since the thread in the thread pool does not synchronize the context, all subsequent calls will randomly use the available threads in the thread pool. Even if you forced it by setting parameter of .ConfigureAwait () to true, it would not help. Please pay attention to the potential impact of such calls: you will lose the ability to go return to the main thread contexts.
Is there another way to have synchronization context and run steps in thread pool?
Execute with Mixed Scheduler
In order to address the questions raised by the end of the previous section, I have designed this use case:
Assume that following steps are demanded in a business logic:
a. computing n! (n=150)
b. 1 seconds waiting
c. output
So we get the following code (in order to expand the execution load, I added sleep time in loops):
public void CPUBigHead(int n)
{
double result = 1;
LogToTUnityConsole(result, $"n!(n={n}) start");
for (int i = 1; i <= n; i++)
{
result = result * i;
Thread.Sleep(10);
}
LogToTUnityConsole(result, $"n!(n={n}) stop");
Thread.Sleep(1000);
//Texture change logic puts here
LogToTUnityConsole(result, $"n!(n={n}) output");
}
n! calculation is a CPU-intensive operation. We want it executing in other threads, and output the results in the UI layer called texture changes, you need to manipulate the thread Id is 1.
On this basis, I modified synchronized code into async to achieve.
public async Task CPUBigHeadAsync(int n)
{
double result = 1;
await Task.Run(() =>
{
LogToTUnityConsole(result, $"n!(n={n}) start");
for (int i = 1; i <= n; i++)
{
result = result * i;
Thread.Sleep(10);
}
LogToTUnityConsole(result, $"n!(n={n}) stop");
}); //Scheduler will contiue execution in main thread here
await Task.Delay(1000);
//Texture change logic puts here
LogToTUnityConsole(result, $"n!(n={n}) output");
}
> Get the following results:
Can see that we make sure that CPU-intensive operations are not running in the main thread. This modification ensures a relatively smooth rendering update.
Comparison summary
From the above examples, we can see that the new Task API / async syntax brings new optimization possibilities for Unity. Old Coroutine mechanisms can be logically split into small sections, but it have less power to control each part running in thread freely. With new Task API you can easily do operations in a thread pool, and make calculations easier.
In addition, Task/await also has some other advantages
· Highly integrated with the UWP API, you can use the simple await/AsTask () extension method to convert.
· To maintain async job's lifetime at the Application level.。
· Control timer/manual interruption of the execution tree by CancellationToke
· Fast package of Event/thread/IAsyncResult, provides a simple unified calling pattern.
In this article we are not going to explain all of these usage. If you have needs to learn more, see Asynchronous Programming Patterns。
As I mentioned at the beginning of this article, new member, Task/async will exist with Coroutine side by side, and the ecosystem will need them interactive with each other. So how to interactive one to another ? Task Coroutine Interactive
>interactive with each other,means that we are able to Coroutine call the Async method, as well as the calling Coroutine in the Async method.
Examples of concurrent execution and no different from basic calls, here we show you how to call one another and calling the next one after the implementation of process implementation.
Async Method Calls a Coroutine And Wait for Completion Code: TaskAwaitACoroutine
If you want to call a Coroutine and in the async methods, we need to wrap the Coroutine implementation to a simple Task.
There are no callbacks or wait handle in YeildInstruction class of Unity. So we need a parent's Coroutine method to monitor completion of the target Coroutine.
IEnumerator tempCoroutine(IEnumerator coro, System.Action afterExecutionCallback)
{
yield return coro;
afterExecutionCallback();
}
At this point we can use the TaskCompeletionSource to encapsulate Coroutine into a Task
public async Task TaskAwaitACoroutine()
{
await TaskAsyncCountDown(2, "precoro");
var tcs = new System.Threading.Tasks.TaskCompletionSource<object>();
StartCoroutine(
tempCoroutine(
CoroutineCountDown(3, "coro"),
() => tcs.TrySetResult(null))); await tcs.Task;
await TaskAsyncCountDown(2, "postcoro");
}
Execution::
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
var t = TaskAwaitACoroutine();
}
As you can see, after the execution of CoroutineCountDown "Coro", the scheduler started to execute the TaskAsyncCountDown "postcoro"
Coroutine Call Task And wait for completion
Unlike how the Async method call Coroutine, Coroutine calls async will check if the Task is complete or interrupted with a regular Update (). This checking is managed by WaitUntil class, which is also a Yield Instruction. Dispatcher will continue execution after the WaitUntil is done, and execute next object in sequence in main thread.
public IEnumerator CoroutineAwaitATask()
{
yield return CoroutineCountDown(2, "pretask");
var task = TaskAsyncCountDown(3, "task");
yield return new WaitUntil(() => task.IsCompleted || task.IsFaulted || task.IsCanceled);
//Check task's return;
if (task.IsCompleted)
{
LogToTUnityConsole("TDone", "task");
}
yield return CoroutineCountDown(2, "posttask");
}
Execution:
void Start()
{
stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
StartCoroutine(CoroutineAwaitATask());
}
You can see, after the executions of the TaskAsyncCountDown "task" Async method, Unity dispatcher continued to execute the CoroutineCountDown "posttask"
Advanced packaging: CoTaskHelper
Code snippets above shows us how to make Task and Coroutine call each other. So do we have a way of simplifying this process, makes operations more productive?
Because we were using the. NET 4.6 subset already , wrapping these operations into extension methods is very easy. So I created CoTaskHelper class. to simplify calling.
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
public static class CoTaskHelper
{
public static async Task ExecuteCoroutineAsync(this MonoBehaviour monoBehavior, IEnumerator coroutine)
{
var tcs = new System.Threading.Tasks.TaskCompletionSource<object>();
monoBehavior.StartCoroutine(
tempCoroutine(
coroutine,
() => tcs.TrySetResult(null)));
await tcs.Task;
}
static IEnumerator tempCoroutine(IEnumerator coro, System.Action afterExecutionCallback)
{
yield return coro;
afterExecutionCallback();
}
public static IEnumerator WaitAsCoroutine(this Task task)
{
yield return new WaitUntil(() => task.IsCompleted || task.IsFaulted || task.IsCanceled);
}
}
The code in last section can be simplified as
public async Task TaskAwaitACoroutineWithEx()
{
await TaskAsyncCountDown(2, "precoro");
await this.ExecuteCoroutineAsync(CoroutineCountDown(3, "coro"));
await TaskAsyncCountDown(2, "postcoro");
}
public IEnumerator CoroutineAwaitATaskWithEx()
{
yield return CoroutineCountDown(2, "pretask");
var task = TaskAsyncCountDown(3, "task");
yield return task.WaitAsCoroutine();
LogToTUnityConsole("TDone", "task");
yield return CoroutineCountDown(2, "posttask");
}
For clarity, more native flavour.
Summary
In this article, we briefly introduce and compare the Unity native Coroutine programming approach and the .NET 4.6 subset + TAP programming approach introduced in the Unity 2017.1, also release and provide examples of code that two methods interact with each other. Hopefully these examples can inspire readers and become the help of using Unity to increase productivity.
P.S.
After practice with projects, I have written a better version, and added YeildInstruction support. Please feel free give me feedback in comments :)
/// <summary>
/// Coroutine and Task Async Interactive Helper
/// Based on artical https://blogs.msdn.microsoft.com/appconsult/2017/09/01/unity-coroutine-tap-en-us/
/// </summary>
public static class CoroutineTaskHelper
{
/// <summary>
/// Translate a coroutine to Task and run. It needs a Mono Behavior to locate Unity thread context.
/// </summary>
/// <param name="monoBehavior">The Mono Behavior that managing coroutines</param>
/// <param name="coroutine">the coroutine that needs to run </param>
/// <returns>Task that can be await</returns>
public static async Task StartCoroutineAsync(this MonoBehaviour monoBehavior, IEnumerator coroutine)
{
var tcs = new TaskCompletionSource<object>();
monoBehavior
.StartCoroutine(
emptyCoroutine(
coroutine,
tcs));
await tcs.Task;
}
/// <summary>
/// Translate a YieldInstruction to Task and run. It needs a Mono Behavior to locate Unity thread context.
/// </summary>
/// <param name="monoBehavior">The Mono Behavior that managing coroutines</param>
/// <param name="yieldInstruction"></param>
/// <returns>Task that can be await</returns>
public static async Task StartCoroutineAsync(this MonoBehaviour monoBehavior, YieldInstruction yieldInstruction)
{
var tcs = new TaskCompletionSource<object>();
monoBehavior
.StartCoroutine(
emptyCoroutine(
yieldInstruction,
tcs));
await tcs.Task;
}
/// <summary>
/// Wrap a Task as a Coroutine.
/// </summary>
/// <param name="task">The target task.</param>
/// <returns>Wrapped Coroutine</returns>
public static CoroutineWithTask<System.Object> AsCoroutine(this Task task)
{
var coroutine = new WaitUntil(() => task.IsCompleted || task.IsFaulted || task.IsCanceled);
return new CoroutineWithTask<object>(task);
}
/// <summary>
/// Wrap a Task as a Coroutine.
/// </summary>
/// <param name="task">The target task.</param>
/// <returns>Wrapped Coroutine</returns>
public static CoroutineWithTask<T> AsCoroutine<T>(this Task<T> task)
{
return new CoroutineWithTask<T>(task);
}
private static IEnumerator emptyCoroutine(YieldInstruction coro, TaskCompletionSource<object> completion)
{
yield return coro;
completion.TrySetResult(null);
}
private static IEnumerator emptyCoroutine(IEnumerator coro, TaskCompletionSource<object> completion)
{
yield return coro;
completion.TrySetResult(null);
}
/// <summary>
/// Wrapped Task, behaves like a coroutine
/// </summary>
public struct CoroutineWithTask<T> : IEnumerator
{
private IEnumerator _coreCoroutine;
/// <summary>
/// Constructor for Task with a return value;
/// </summary>
/// <param name="coreTask">Task that need wrap</param>
public CoroutineWithTask(Task<T> coreTask)
{
WrappedTask = coreTask;
_coreCoroutine = new WaitUntil(() => coreTask.IsCompleted || coreTask.IsFaulted || coreTask.IsCanceled);
}
/// <summary>
/// Constructor for Task without a return value;
/// </summary>
/// <param name="coreTask">Task that need wrap</param>
public CoroutineWithTask(Task coreTask)
{
WrappedTask = Task.Run(async () =>
{
await coreTask;
return default(T);
});
_coreCoroutine = new WaitUntil(() => coreTask.IsCompleted || coreTask.IsFaulted || coreTask.IsCanceled);
}
/// <summary>
/// The task have wrapped in this coroutine.
/// </summary>
public Task<T> WrappedTask { get; private set; }
/// <summary>
/// Task result, if it have. Calling this property will wait execution synchronously.
/// </summary>
public T Result { get { return WrappedTask.Result; } }
/// <summary>
/// Gets the current element in the collection.
/// </summary>
public object Current => _coreCoroutine.Current;
/// <summary>
/// Advances the enumerator to the next element of the collection.
/// </summary>
/// <returns>
/// true if the enumerator was successfully advanced to the next element; false if
/// the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
return _coreCoroutine.MoveNext();
}
/// <summary>
/// Sets the enumerator to its initial position, which is before the first element
/// in the collection.
/// </summary>
public void Reset()
{
_coreCoroutine.Reset();
}
}
}
Comments
- Anonymous
October 03, 2017
Thanks for the article. Very clean codes to explain it all.- Anonymous
October 03, 2017
The comment has been removed- Anonymous
October 13, 2017
Thanks Chris. It is truly a bug (my typo) and you helped me out. Correcting it.you may enjoy the new version i will append today.
- Anonymous
- Anonymous
- Anonymous
December 09, 2017
You don't need to await tcs.Task inside function, just return tcs.Task and remove async from method ;)- Anonymous
January 29, 2018
in some case, yes. in some cases i still need the task got finshed to run following steps, like logging, and in some other cases, i need the await tcs.Task to keep the async keyword in the method declarations.Async writing is not a "have to do", but a "would like to do" to me, so I would rather not change the style. I believe Roslyn and JIT can do fine job in these cases.
- Anonymous