For anyone struggling with tasks, I’m throwing in my AsyncRoot utility that I’m using everywhere to start new tasks from non-async contexts, and safely log their exceptions.
Uses UniTask for most of its functionalities. Been using if for over 1 year with success, true life saver.
Updated code:
Updated Code
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using Cysharp.Threading.Tasks;
using LibCore.Sys.Logging;
namespace LibCore.Threading
{
/// <summary>
/// Helper to wrap execution of UniTasks from a sync method while handling any exceptions.
/// The pattern "DoAsync().Forget()" also works, but centralizing these calls might prove useful in the future
/// TODO run all tasks as bound, but those that don't explicitly request that will be bound to a unique, persistent, hidden-in-hierarchy gameobject, so as to minimize dangling tasks from leaking from playmode
/// </summary>
public static class AsyncRoot
{
static AsyncRoot()
{
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
public static void RunForget(Func<UniTask> taskFactory) => _ = Run(taskFactory);
public static Task Run(Func<UniTask> taskFactory)
{
var task = taskFactory().AsTask();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
public static Task<T> Run<T>(Func<UniTask<T>> taskFactory)
{
var task = taskFactory().AsTask();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
/// <summary>
/// For running bound to an object use GetCancellationTokenOnDestroy() extension at task creation and then the standard Task.Run(.., ..)
/// </summary>
/// <param name="taskFactory"></param>
public static Task<T> Run<T>(Func<Task<T>> taskFactory)
{
var task = taskFactory();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
public static Task Run(Func<Task> taskFactory)
{
var task = taskFactory();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
public static void RunBound(GameObject boundTo, Func<UniTask> taskFactory)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory);
}
public static void RunBound(Component boundTo, Func<UniTask> taskFactory)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory);
}
// Not tested
public static Task<(bool isCancelled, T result)> RunBound<T>(Component boundTo, Func<UniTask<T>> taskFactory)
{
return RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory);
}
/// <summary>
/// Note you need to check for <see cref="continueWith.IsFaulted"/> before assuming success
/// </summary>
public static void RunBound(GameObject boundTo, Func<UniTask> taskFactory, Action<Task> continueWith)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory, continueWith);
}
/// <summary>
/// Note you need to check for <see cref="continueWith.IsFaulted"/> before assuming success
/// </summary>
public static void RunBound(Component boundTo, Func<UniTask> taskFactory, Action<Task> continueWith)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory, continueWith);
}
static void RunInternal(CancellationToken cancellationToken, Func<UniTask> taskFactory, Action<Task> continueWith)
{
RunInternal(cancellationToken, taskFactory, continueWith, TaskContinuationOptions.None);
}
// Not tested
static Task<(bool isCancelled, T result)> RunInternal<T>(CancellationToken cancellationToken, Func<UniTask<T>> taskFactory, Action<Task> continueWith)
{
return RunInternal(cancellationToken, taskFactory, continueWith, TaskContinuationOptions.None);
}
static void RunInternal(CancellationToken cancellationToken, Func<UniTask> taskFactory)
{
RunInternal(cancellationToken, taskFactory, LogException, TaskContinuationOptions.OnlyOnFaulted);
}
// Not tested
static Task<(bool isCancelled, T result)> RunInternal<T>(CancellationToken cancellationToken, Func<UniTask<T>> taskFactory)
{
return RunInternal(cancellationToken, taskFactory, LogException, TaskContinuationOptions.OnlyOnFaulted);
}
static void RunInternal(CancellationToken cancellationToken, Func<UniTask> taskFactory, Action<Task> continueWith, TaskContinuationOptions continuationOptions)
{
var task = taskFactory()
.AttachExternalCancellation(cancellationToken)
.SuppressCancellationThrow()
.AsTask();
task.ContinueWith(continueWith, continuationOptions);
}
// Not tested
static Task<(bool isCancelled, T result)> RunInternal<T>(CancellationToken cancellationToken, Func<UniTask<T>> taskFactory, Action<Task> continueWith, TaskContinuationOptions continuationOptions)
{
var task = taskFactory()
.AttachExternalCancellation(cancellationToken)
.SuppressCancellationThrow()
.AsTask();
task.ContinueWith(continueWith, continuationOptions);
return task;
}
/// <summary>
/// IMPORTANT: Using LogException because throwing doesn't work on non-main thread (might try calling a dispatcher, but letting it as it is for now)
/// </summary>
/// <param name="task"></param>
static void LogException(Task task)
{
Debug.LogException(task.Exception);
}
static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
// This is called on GC, if the tasks didn't have a ContinueWith for Failure, nor had anyone handling their exceptions.
// This shouldn't normally happen if all tasks are started from this class, but it's good to log them
L.Deb(e.Exception);
}
}
}
Original code (ignore it, use the above pls):
Code
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using Cysharp.Threading.Tasks;
namespace LibCore.Threading
{
/// <summary>
/// Helper to wrap execution of UniTasks from a sync method while handling any exceptions.
/// The pattern "DoAsync().Forget()" also works, but centralizing these calls might prove useful in the future
/// TODO run all tasks as bound, but those that don't explicitly request that will be bound to a unique, persistent, hidden-in-hierarchy gameobject, so as to minimize dangling tasks from leaking from playmode
/// </summary>
public static class AsyncRoot
{
public static void RunForget(Func<UniTask> taskFactory) => _ = Run(taskFactory);
public static Task Run(Func<UniTask> taskFactory)
{
var task = taskFactory().AsTask();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
public static Task<T> Run<T>(Func<UniTask<T>> taskFactory)
{
var task = taskFactory().AsTask();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
/// <summary>
/// For running bound to an object use GetCancellationTokenOnDestroy() extension at task creation and then the standard Task.Run(.., ..)
/// </summary>
/// <param name="taskFactory"></param>
public static Task<T> Run<T>(Func<Task<T>> taskFactory)
{
var task = taskFactory();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
public static Task Run(Func<Task> taskFactory)
{
var task = taskFactory();
task.ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
return task;
}
public static void RunBound(GameObject boundTo, Func<UniTask> taskFactory)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory);
}
public static void RunBound(Component boundTo, Func<UniTask> taskFactory)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory);
}
// Not tested
public static Task<(bool isCancelled, T result)> RunBound<T>(Component boundTo, Func<UniTask<T>> taskFactory)
{
return RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory);
}
/// <summary>
/// Note you need to check for <see cref="continueWith.IsFaulted"/> before assuming success
/// </summary>
public static void RunBound(GameObject boundTo, Func<UniTask> taskFactory, Action<Task> continueWith)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory, continueWith);
}
/// <summary>
/// Note you need to check for <see cref="continueWith.IsFaulted"/> before assuming success
/// </summary>
public static void RunBound(Component boundTo, Func<UniTask> taskFactory, Action<Task> continueWith)
{
RunInternal(boundTo.GetCancellationTokenOnDestroy(), taskFactory, continueWith);
}
static void RunInternal(CancellationToken cancellationToken, Func<UniTask> taskFactory, Action<Task> continueWith)
{
RunInternal(cancellationToken, taskFactory, continueWith, TaskContinuationOptions.None);
}
// Not tested
static Task<(bool isCancelled, T result)> RunInternal<T>(CancellationToken cancellationToken, Func<UniTask<T>> taskFactory, Action<Task> continueWith)
{
return RunInternal(cancellationToken, taskFactory, continueWith, TaskContinuationOptions.None);
}
static void RunInternal(CancellationToken cancellationToken, Func<UniTask> taskFactory)
{
RunInternal(cancellationToken, taskFactory, LogException, TaskContinuationOptions.OnlyOnFaulted);
}
// Not tested
static Task<(bool isCancelled, T result)> RunInternal<T>(CancellationToken cancellationToken, Func<UniTask<T>> taskFactory)
{
return RunInternal(cancellationToken, taskFactory, LogException, TaskContinuationOptions.OnlyOnFaulted);
}
static void RunInternal(CancellationToken cancellationToken, Func<UniTask> taskFactory, Action<Task> continueWith, TaskContinuationOptions continuationOptions)
{
var task = taskFactory()
.AttachExternalCancellation(cancellationToken)
.SuppressCancellationThrow()
.AsTask();
task.ContinueWith(continueWith, continuationOptions);
}
// Not tested
static Task<(bool isCancelled, T result)> RunInternal<T>(CancellationToken cancellationToken, Func<UniTask<T>> taskFactory, Action<Task> continueWith, TaskContinuationOptions continuationOptions)
{
var task = taskFactory()
.AttachExternalCancellation(cancellationToken)
.SuppressCancellationThrow()
.AsTask();
task.ContinueWith(continueWith, continuationOptions);
return task;
}
/// <summary>
/// IMPORTANT: Using LogException because throwing doesn't work on non-main thread (might try calling a dispatcher, but letting it as it is for now)
/// </summary>
/// <param name="task"></param>
static void LogException(Task task)
{
Debug.LogException(task.Exception);
}
}
}