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"

unity 20170 ℃ f 彐 Projects Learn Project name* AsyncExample Location* D:\HoloLabs Organization* waynebaby + NEW 1 OPEN MY ACCOUNT . 3D 〔 〕 2D Add Asset Package . Enable unity Analytics ( 2 》 C 引 'C 乸 Create proJect

3. Click the "Create Project" button

4. Click on the menu "File-> Build Settings"

unity 20170 ℃ f 彐 〔 545 - Untitled - AsyncExample PC, Mac & Linux Standalone < OXI 1 > File Edit Assets GameObJect Component Window Help New Scene Open Scene Save Scenes Save Scene as … New Projectm Open Projectm Save Project Build Settingsm Build & Run Ctrl+N Ctrl+O *Scene Ctrl+S Ctrl+Shift+S Ctrl+Shift+B Ctrl+B 《 G m e Asset Store

5. Click "Player Settings" on the bottom.

Look at your  "Inspector" window, there will be some property tabs. Unfold "Other Settings" tab.

0 Inspector PlayerSettings Company N m e p rO d 凵 Ct N m e Default I CO n Default C 凵 rS 0 r C 凵 rs 0 r Hotspot DefaultCompany AsyncExample Settings for PC, M C & Li n 凵 X Sta n d 引 0 n e Re 、 0 《 uti 0 n a n d P re 、 e ntati 0 n Splash Image Other Settings Rendering C 引 0 r Space* Auto Graphics API for 喜 in 《 寻 Auto Graphics API for MacV Auto Graphics API for Static Batching Dynamic Batching G P 凵 Skinning Graphics ] 0 b S (Experimentn Vi 《 t 凵 引 Reality Suppofted 0 M a ( App Store Options 巳 凵 n 引 e I d e ntifi e r 巳 凵 i 尾 Category com Company . p ro d 凵 CtN m e public.app-category m e S M C App Store Val i d ati 0 n 0 Configuration Scrlptlng R 凵 ntl m e Ve rSl 0 n Scripting 巳 Ck e n d Api Compatibility Le V 引 D i S b H 喜 Statistics* Scripting D efi n e Symbols* Asset La b e IS 。 国 ET 2.0 S u

6. Find the "Configuration" property group.  Change the value of "Scripting Runtime Version *"  "Stable (.NET 3.5 Equivalent)" to "Experinmental (.NET 4.6 Equivalent) "

Configuration SCI'lptlng R 凵 Intl m V 「 引 0 n Scripting 巳 Ck e n d Api Compatibility Le V 引 D i S b H 喜 Statistics* Scripting D efi n e Symbols* Acti V e Input Handling* Sta b 门 ET 3 E q u 吲@nth Stable (.NET 3 巧 Equivalent 〕 Experimental (.NET 4 乇 Equivalent

7. Unity Editor needs a complete reboot after your modification, you will see following message.

Resta 戊 required 0 Changing scripbng runtime V 引 on requres 已 restart Of 巴 Editor to 什 匚 t Do you Wish to pro 上 巴 带 Restart

Select "Restart" to restart the Unity.
After restart, at the same location, properties should be

Configuration Scrlptlng R 凵 ntl m e Ve rSl 0 n Scripting 巳 Ck n d Api Compatibility Le V 引 D i S b H 喜 Statistics* Scripting D efi n e Symbols* 。 国 ET 4 石 E q u 。 国 ET 4 乇

 

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"

Favorites Q All 卜 te 引 s 0 . All prefabs 《 aAll Models 《 All Scripts · 0 Assets 0 Scrtps As 0t5 . SC-rips 的 AsyncExample

9. In the "Hierarchy" window, create a new, empty object named "ConsoleScripts"

Untitled* Main C a m e 「 D e on 引 Light Con 不 0 Scnpt

10. Drag the script you have created into "ConsoleScripts" Game Object

0 Inspector 寻 Console-scripts Tag untagged 丿 Transform p 0 S 《 乜 0 n Rot 乜 on S cal e . I. 2 3 3 9 7 2 丫 I. 7 21617 . 9 7 9194 @ Example (Script) 的 AsyncExample Add Component

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")); 
    } 

C 0 n S 0 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 巳 S i CC 0 C 引 《 巳 S i CC 0 C 引 《 巳 S i CC 0 C 引 《 巳 S i CC 0 C 引 《 10 0 5 2 018 3 0 31 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1

 

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.

C 0 n S 0 C 0 rO Lltln e C 0 凵 ntD 0 n unityEngine Debug:Log(object) 0 0 「 0 Lltln 0 0 凵 ntO 0 喜 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 巳 a S i CC 0 C 引 IA 巳 S i CC 0 C 引 旧 巳 S i CC 0 C 引 IA 2 巳 S i CC 0 C 引 旧 巳 S i CC 0 C 引 IA 巳 S i CC 0 C 引 旧 巳 S i CC 0 C 引 IA 巳 S i CC 0 C 引 旧 10 0 3 2 010 2 012 3 0 24 3 0 2 6 i n th re a d 1 i n th d 1 i n th d 1 i n th re d 1 i n th d 1 i n th re d 1 i n th d 1 i n th re d 1

 

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:

C 0 n S 引 e C 0 rO Lltln e C 0 凵 ntD 0 n unityEngine Debug:Log(object) C 0 「 0 Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 喜 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 喜 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 喜 n unityEngine Debug:Log(object 〕 巳 a S i CC 0 C 引 IA 巳 S i CC 0 C 引 IA 2 巳 S i CC 0 C 引 IA 巳 S i CC 0 C 引 IA 巳 S i CC 0 C 引 旧 巳 S i CC 0 C 引 旧 巳 S i CC 0 C 引 旧 巳 S i CC 0 C 引 旧 2 011 3 0 2 0 4 0 31 5 041 6 0 5 2 7 0 6 3 i n th re a d 1 i n th d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1

 

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.

Solution stopwatch = new System .Diagnostics .Stopwatch( ), stopwatch 。 Start() ; "8asicAs ncCaII" . ] TaskAs ncCountDown 彐 . (awaitable) Task AsyncExample•TaskAsyncCountDown(int 〔 0 凵 n 《 [string flag public I Usage : await TaskAsyncCountDown(m); Because this 〔 引 《 is not awaited execution Of the current method continues before the 〔 引 《 is completed. Consider applying the w 引 t 《 0 巳 儲 tor to the result Of the 〔 引 上 Show potential fixes (Alt+Enter or Ctrl+.)

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

C 0 n S 0 TaskAsyncCountDonn BasicAsyncCall unityEngine Debug:Log(object) TaskAsyncCountOonn BasicAsyncCall unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall unityEngine Debug:Log(object 〕 10 31 2 0 5 7 3 0 6 6 i n th re a d 1 i n th d 1 i n th re d 1 i n th re d 1

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); 
        }
    }

C 0 n S 0 TaskAsyncCountDonn BasicAsyncCall unityEngine Debug:Log(object) TaskAsyncCountOonn BasicAsyncCall unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall unityEngine Debug:Log(object 〕 10 2 2 2 0 2 5 3 0 2 6 i n th re a d 1 i n th d 9 5 i n th re d 94 i n th re d 9 5

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");
    }

C 0 n S 0 TaskAsyncCountDonn BasicAsyncCallA unityEngine Debug:Log(object) TaskAsyncCountOonn BasicAsyncCalla unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallA 2 unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCalla unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallA unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCalla unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallA unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCalla unityEngine Debug:Log(object 〕 10 2 9 10 31 2 0 3 9 2 04 0 3 04 9 3 0 5 0 i n th re a d 1 i n th d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1

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 C 0 n S 0 TaskAsyncCountDonn BasicAsyncCallA unityEngine Debug:Log(object) TaskAsyncCountOonn BasicAsyncCallA 2 unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallA unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallA unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallB unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallB unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallB unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCallB unityEngine Debug:Log(object 〕 10 2 6 2 04 2 410 3 5134 614 9 716 5 i n th re a d 1 i n th d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1

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")); 
    }

C 0 n S 0 TaskAsyncCountDonn BasicAsyncCall 10 unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall 10 21 unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall 2 0 2 3 unityEngine Debug:Log(object 〕 TaskAsyncCountOonn BasicAsyncCall 3 0 24 unityEngine Debug:Log(object 〕 i n th re a d 111 i n th d 114 i n th re d 111 i n th d 114
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");
    }

C 0 n S 0 CPUBigHead n n = 150 〕 unityEngine : CPIJ8igHead unityEngine Debug:Log(object 〕 CPIJ8igHead unityEngine Debug:Log(object 〕 1 i n th re a d 1 n ! 〔 n = 15 的 Stop 5 013 3 8 3 9 5 644 5 8 5 E + 2 6 2 n ! 〔 n = 150 〕 output 5 013 3 8 3 9 5 644 5 8 5 E + 2 6 2 15 6 5 2 5 6 9 i n th d 1 i n th re d 1

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:  C 0 n S 0 CPUBigHeadAsync n 区 n = 150 〕 unityEngine Debug:Log(object) CPIJ8igHeadAsync unityEngine Debug:Log(object 〕 CPIJ8igHeadAsync unityEngine Debug:Log(object 〕 1 10 i n th re a d 216 n ! 〔 n = 150 〕 Stop 5 013 3 8 3 9 5 644 5 8 5 E + 2 6 2 n ! 〔 n = 150 〕 output 5 013 3 8 3 9 5 644 5 8 5 E + 2 6 2 15 7 7 2 6 3 0 i n th d 216 i n th re d 1

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(); 
    }

 

C 0 n S 0 TaskAsyncCountOonn unityEngine Debug:Log(object 〕 TaskAsyncCountOonn unityEngine Debug:Log(object 〕 TaskAsyncCountOonn unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 TaskAsyncCountOonn unityEngine Debug:Log(object 〕 TaskAsyncCountOonn unityEngine Debug:Log(object 〕 TaskAsyncCountOonn unityEngine Debug:Log(object 〕 104 0 2 0 64 3 0 7 2 4 0 8 5 5118 615 8 717 6 8 2 0 3 9 2 0 5 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1

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()); 
    }

C 0 n S 0 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n pretask pretask pretask unityEngine Debug:Log(object 〕 TaskAsyncCountOonn S k unityEngine Debug:Log(object 〕 TaskAsyncCountOonn S k unityEngine Debug:Log(object 〕 TaskAsyncCountOonn S k unityEngine Debug:Log(object 〕 TaskAsyncCountOonn S k unityEngine Debug:Log(object 〕 C 0 rO uti n eAn itATa S k unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 C 0 rO Lltln e C 0 凵 ntO 0 n unityEngine Debug:Log(object 〕 S k TO 0 n e post-task post-task post-task 1 10 0 6 2 0 0 6 3 012 4 0 3 0 5 0 3 8 6 0 5 2 7 0 6 2 8 0 7 0 9116 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1 7 0 6 2 i n th re d 1 i n th re d 1 i n th re d 1 i n th re d 1

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();
            }
        }
    }