AceLand Unity Packages
  • Home
  • Getting Started
    • Installation
    • Project Settings
    • Architecture Graph
    • Development Level
  • Tutorial
    • Create Your Package
    • Create Project Settings
  • Packages
    • Library
      • Change Log
      • Editor Tools
      • Mono
        • Follow Object
        • Singleton
      • Attributes
        • Conditional Show
        • Inspector Button
        • ReadOnly Field
      • Build Leveling
      • CVS
      • DataTools
      • Disposable Object
      • Extensions
      • Json
      • Kalman Filter
      • Optional
      • Project Setting
      • Serialization Surrogate
      • Utils
    • Event Driven
      • Change Log
      • Event Bus
      • Signal
    • Input
      • Change Log
    • Memento Service
      • Change Log
    • Node Framework
      • Change Log
      • Mono Node
    • Node For Mono (dev)
    • Player Loop Hack
      • Change Log
    • Pool
      • Change Log
    • Security (dev)
    • States
      • Change Log
    • Task Utils
      • Change Log
    • WebRequest
      • Change Log
Powered by GitBook
On this page
  • In One Line
  • Overview
  • Package Info
  • Why Use It
  • How It Works
  • Usage Example
  • Easy Safe Task
  • Work with Coroutine
  • Utilities
  • Best Practices
  1. Packages

Task Utils

A safe and elegant solution for handling C# Tasks and asynchronous operations in Unity

In One Line

Threading wizardry: Promise made, Promise kept!

Overview

While C# Tasks are powerful, they require careful handling in Unity's single-threaded environment. Task Utils provides a robust framework for managing asynchronous operations safely, with a focus on exception handling and main thread synchronization.

Promise Awaiter transforms traditional async/await patterns into a more manageable and safer approach.

This implementation ensures that:

  • Async operations can safely return to the main thread

  • Unity API calls are always thread-compliant

  • No threading-related crashes or race conditions

  • Predictable execution order in the Unity lifecycle

  • PromiseAgent will run Coroutine cross different scene

  • Coroutine can be converted to Task

Package Info

display name

AceLand Task Utils

package name

latest version

1.0.9

namespace

git repository

dependencies


Why Use It

The most important reason is thread-safe and exception-safe for Unity.

Traditional exception handling in production builds can be problematic:

  • Unhandled exceptions can freeze critical application flows

  • Users are left with no feedback when errors occur

  • Debug information is often lost in production builds

Promise Awaiter solves these issues by:

  • Keeping your application running despite errors

  • Enabling user-friendly error handling

  • Maintaining proper error logging for debugging

  • Ensuring operations stay on the main thread when needed

Developer Benefits:

  • No more await/try-catch complexity

  • Clean, chainable syntax

  • Safe main thread synchronization

  • Integrated with other AceLand packages

  • Ideal for both development and production environments


How It Works

Task can be converted to Promise style chain methods:

  • Then(async action) - completed with success result

  • Then(action) - completed with success result

  • Catch(action) - completed with exception

  • Final(action) - completed either success or exception

Callbacks will pass to Unity Main Thread Dispacher and queued to execute during the Initialization phase of Unity's PlayerLoop. This guarantees that all handlers always run on the main thread. Developers are free from manual thread synchronization or checking IsMainThread, and safe to access Unity APIs, UI elements, and GameObjects.

On case of success, async action will be invoked first, and wait until completed. If completed with exception, it will jump to Catch process. Then(action) will not be invoked in this case.

In Then(async action), Exception will not be throw if await a Promise. Please await a native Task instead.

Then and Catch will not be invoked if Task or Promise is canceled or disposed.

Final action will not be invoked if Promise is disposed.

Please prevent from deep nested Promise blocks for clean code.


Usage Example

using System;
using System.Threading;
using System.Threading.Tasks;
using AceLand.TaskUtils;
using UnityEngine;

public class TaskUtilsExample : MonoBehaviour
{
    [SerializeField] private int target = 5_000;

    private void Start()
    {
        TaskTest().Then(result => Debug.Log($"Task Finish in {result:f3}s"))
            .Catch(e => Debug.LogError(e, this))
            .Final(() => Debug.Log("Task Completed"));
    }

    private async Task<double> TaskTest()
    {
        var token = Promise.ApplicationAliveToken;
        var start = DateTime.Now;
        var count = 0;
        
        while (count < target && !token.IsCancellationRequested)
        {
            count++;
            await Task.Yield();
        }

        return (DateTime.Now - start).TotalSeconds;
    }
}
// create an array Promise<int>[]
// ** type of tasks are all Task<int>
// if return value type of Task<T> must be same as Promise<T>
var promises = new Promise<int>[]
{
    // all tasks should be Task<int> as Promise<int>
    Task1().Then(OnSuccess1),
    Task2().Then(OnSuccess2),
    Task3().Then(OnSuccess3),
    Task4().Then(OnSuccess4),
};

int sum = 0;
// you can await a promise if necessary
await Promise.WhenAll(promises)
    .Then(result => sum += result)    // get value as int
    .Catch(e => Debug.LogError(e, this))
    .Final(() => Debug.Log($"Final with {sum}");

// cancel all promise in array
promises.Cancel(); 

// dispose all promise in array
promises.Dispose();
// create an array Promise[]
// ** type of tasks will convert to a Promise
// no value will be return
var promises = new Promise[]
{
    Task1().Then(OnSuccess1),    // Task<int> as Promise
    Task2().Then(OnSuccess2),    // Task<float> as Promise
    Task3().Then(OnSuccess3),    // Task as Promise
    Task4().Then(OnSuccess4),    // Task<bool> as Promise
};

// you can await a promise if necessary
await Promise.WhenAll(promises)
    .Then(() => Debug.Log("Tasks Completed"))    // no value will be passed
    .Catch(e => Debug.LogError(e, this))
    .Final(() => Debug.Log($"Final");

// cancel all promise in array
promises.Cancel(); 

// dispose all promise in array
promises.Dispose();
Task1().Then(async () =>
    {
        // do not use Promise here
        // only native Task exception can be throw
        await Task2();
        OnTask2Success();
    })
    .Then(OnTask1Success)    // will not invoke if Task2 got exception
    .Catch(OnError)    // any exception will be invoked.
    .Final(OnFinal)    // will not invoke if canceled or disposed
// Wait for seconds
Promise.WaitForSeconds(1.5f)
    .Then(() => Debug.Log("1.5s is passed");

// Wait for condition
bool ready = false;
Promise.WaitUntil(() => ready)
    .Then(() => Debug.Log("I'm ready");

// Run stuff on MainThread in a Multi-Threading Task
Action RequireOnMainThread;
Task.Run(() => 
    {
        // with Extension (Recommend)
        RequireOnMainThread?.EnqueueToDispatcher();
        // or Promise API
        // if (RequireOnMainThread != null)
        //     Promise.EnqueueToDispatcher(RequireOnMainThread);
    },
    token
);

Promise Awaiter will catch all exception, and pass Then Catch Final actions to Main Thread Dispatcher. That means the task will be finished without any return.

If awaiting promise in Then async block, the whole promise may be finished without exception return. In exception case, the process will be hold on the await promise.


Easy Safe Task

A lazy way to run Task or heavy stuff.

public class YourComponent : MonoBehaviour
{
    private void Start()
    {
        // Very Heavy Job will run on Thread Pool
        Promise.SafeRun(VeryHeavyJob)
            .Then(() => Debug.Log("Heavy Job Completed", this))
            .Catch(Debug.LogError);
    }
    
    private void VeryHeavyJob()
    {
        // really heavy stuff
    }
}

SafeRun will run all stuff in Thread Pool. Most Unity API cannot be excute.

Please use Coroutine if Unity API is necessary. See next section below.


Work with Coroutine

A lot of Unity APIs are supporting on only Main Thread that processes need to switch between main thread and thread pool. This makes Coroutine is the only choice to do heavy jobs with Unity APIs.

However it is not thread safe by yield return a Task in coroutine. Task can be yield return in coroutine, but exception cannot be handled. And value cannot be returned.

Promise Handle is one of the solution.

Promise is already a thread safe handler. By chaining with .Catch() after a Task, exceptions will be catched by handler.

// yield return a Promise/Task
public class YourComponent : MonoBehaviour
{
    private IEnumerator YourCoroutine()
    {
        bool result = false;
        
        // safe way to handle exception with Promise
        yield return Promise.SafeRun(() => result = VeryHeavyJob())
            .Catch(Debug.LogError);
            
        string msg = result
            ? "Result is very good"
            : "Resout is not good";
        Debug.LogWarning(msg, this);
    }
    
    private bool VeryHeavyJob()
    {
        // really heavy stuff
        return true;
    }
}
// Keep Coroutine running even scene is unloaded
Coroutine().RunCoroutine();

// Run as Task and chain with Promise
Coroutine().AsTask
    .Then(() => Debug.Log("Coroutine Completed"))
    .Catch(ex => Debug.LogError($"Coroutine Catch: {ex}"));

// your coroutine
private IEnumerator Coroutine()
{
    yield return null;
    
    var counter = 0;
    while (counter < 5)
    {
        Debug.Log($"Test1: {counter}");
        yield return new WaitForSecondsRealtime(1);
        counter++;
    }
}

Coroutine or IEnumerable will always run in Main Thread.

AsTask will always return a Task instead of Task<T>. Then will not receive any value from IEnumerator.


Utilities

// get application alive token
CancellationToken aliveToken = Promise.ApplicationAliveToken;

// create Linked Token with alive token
CancellationTokenSource yourTokenSource = new();
CancellationToken linkedToken = Promise.LinkedOrApplicationAliveToken(
    yourTokenSource, out CancellationTokenSource linkedTokenSource);

// enqueue your action to Dispatcher
// action will be invoked on next PlayerLoopState.Initialization
action.EnqueueToDispatcher();
action.EnqueueToDispatcher(arg);
Promise.EnqueueToDispatcher(action);
Promise.EnqueueToDispatcher(action, arg);

// enquene your action with specified PlayerLoopState
action.EnqueueToDispatcher(PlayerLoopState);
action.EnqueueToDispatcher(arg, PlayerLoopState);
Promise.EnqueueToDispatcher(action, PlayerLoopState);
Promise.EnqueueToDispatcher(action, arg, PlayerLoopState);

// add/remove listener on Application Quit
Promise.AddApplicationQuitListener(listener);
Promise.RemoveApplicationQuitListener(listener);

Unity is single-thread environment. Passing actions to dispatcher can confirm thread-safe processes.

using System;
using AceLand.PlayerLoopHack;
using AceLand.TaskUtils;
using UnityEngine;

public class DispatcherTester : MonoBehaviour
{
    private int counter;
    
    private void Update()
    {
        Debug.Log("DispatcherTester -------");

        // run on EndOfFrame
        Promise.WaitForEndOfFrame(PrintEofLog);
        
        // Run on all PlayerLoopState
        var states = (PlayerLoopState[])Enum.GetValues(typeof(PlayerLoopState));
        foreach (var state in states)
            Promise.EnqueueToDispatcher(PrintStateLog, state, state);
        
        counter++;
    }

    private void PrintStateLog(PlayerLoopState state)
    {
        Debug.Log($">> DispatcherTester ({counter}) : {state}");
    }

    private void PrintEofLog()
    {
        Debug.Log($">> DispatcherTester ({counter}) : EOF");
    }
}

️ Important Task Safety Notes

Tasks in Unity Editor pose significant risks during Play Mode:

  • Tasks initiated during Play Mode continue running even after stopping

  • Background tasks persist without proper cleanup

  • Can lead to memory leaks and unexpected behaviors

  1. Always Use Cancellation Tokens

    // Recommended approach
    await YourTask(Promise.ApplicationAliveToken);

  1. Resource Management

    • Properly dispose resources on application quit

    • Use linked tokens for dependent operations

    • Monitor task lifecycles, especially in Editor

Ensure clean task management both in Editor and build environments, preventing resource leaks and unexpected behaviors.


Best Practices

  • Promise Awaiter will catch system exception and pass to Main Thread Patch. This is a thread-safe design for Unity. That's why exception cannot be catch when await Promise or nested Promise.

  • As above, building native Task design is very important. Promise Awaiter is to prevent from unsafe threading code and clear code structure.

  • Work with Coroutine for using Unity APIs in Main Thread but keep heavy works in Thread Pool. Tasks may be split by many little parts for exceptions handling.

Last updated 1 month ago

For native Task design sample, please read from to end of page.

com.aceland.taskutils
AceLand.TaskUtils
com.aceland.library: 1.0.20
com.aceland.playerloophack: 1.0.7
https://github.com/parsue/com.aceland.taskutils.git
WebRequest - Advance Usage