Dotnet's async task programming model makes it much easier to write multi-threaded code, unfortunatly Unity's api's don't
like to be called from non-Unity threads. For example you might have some background task to open a connection and then you
realize you need to access Unity's PlayerPrefs
api to get some key for example.
This utility makes that scenario easier by providing a api for executing code on Unity's syncronization context and handing you back a convient task to track when the code has been executed.
For example:
private async Task BackgroundWorkAsync()
{
// Do some async work.
await Task.Delay(100);
// Here we need to access Unity's player-prefs.
var key = await UnityContext.ExecuteAsync(UnityEngine.PlayerPrefs.GetString, "auth-token");
// Do some more async work.
await Task.Delay(100);
}
UnityContext utility source: (all MIT ofcourse)
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Utility for executing code on the Unity SynchronizationContext.
/// </summary>
public static class UnityContext
{
private class WorkHandle<TIn, TOut>
{
private readonly TaskCompletionSource<TOut> tcs = new TaskCompletionSource<TOut>();
private readonly object workDelegate;
private readonly int workDelegateType;
private readonly TIn input;
public WorkHandle(Action workDelegate, TIn input) :
this(workDelegate, delegateType: 1, input) { }
public WorkHandle(Action<TIn> workDelegate, TIn input) :
this(workDelegate, delegateType: 2, input) { }
public WorkHandle(Func<TOut> workDelegate, TIn input) :
this(workDelegate, delegateType: 3, input) { }
public WorkHandle(Func<TIn, TOut> workDelegate, TIn input) :
this(workDelegate, delegateType: 4, input) { }
public WorkHandle(object workDelegate, int delegateType, TIn input)
{
this.workDelegate = workDelegate;
this.workDelegateType = delegateType;
this.input = input;
}
public Task<TOut> WaitForComplete => tcs.Task;
public static void Execute(object handle)
{
Debug.Assert(handle != null && handle is WorkHandle<TIn, TOut>, "Invalid handle");
Execute((WorkHandle<TIn, TOut>)handle);
}
public static void Execute(WorkHandle<TIn, TOut> workData)
{
Debug.Assert(workData != null && workData.workDelegate != null, "Work-delegate missing");
TOut result = default;
try
{
switch (workData.workDelegateType)
{
case 1:
Debug.Assert(workData.workDelegate is Action, "Non-matching delegate type");
((Action)workData.workDelegate).Invoke();
break;
case 2:
Debug.Assert(workData.workDelegate is Action<TIn>, "Non-matching delegate type");
((Action<TIn>)workData.workDelegate).Invoke(workData.input);
break;
case 3:
Debug.Assert(workData.workDelegate is Func<TOut>, "Non-matching delegate type");
result = ((Func<TOut>)workData.workDelegate).Invoke();
break;
case 4:
Debug.Assert(workData.workDelegate is Func<TIn, TOut>, "Non-matching delegate type");
result = ((Func<TIn, TOut>)workData.workDelegate).Invoke(workData.input);
break;
default:
Debug.Fail("Invalid work-delegate type");
break;
}
workData.tcs.SetResult(result);
}
catch (Exception e)
{
workData.tcs.SetException(e);
}
}
}
private static SynchronizationContext unityContext;
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Initialize()
{
if (SynchronizationContext.Current == null)
throw new NonUnityContextException();
unityContext = SynchronizationContext.Current;
}
/// <summary>
/// Is the Unity SynchronizationContext currently active. If true it means you're allowed
/// to access Unity api's, if false you are not.
/// </summary>
public static bool IsActive
{
get
{
if (unityContext == null)
throw new InvalidOperationException("UnityContext has not been initialized");
return SynchronizationContext.Current == unityContext;
}
}
/// <summary>
/// Throw a exception when the Unity SynchronizationContext is currently not active.
/// </summary>
/// <exception cref="NonUnityContextException">
/// Thrown when called from a non-unity context.
/// </exception>
public static void EnsureActive()
{
if (unityContext == null)
throw new InvalidOperationException("UnityContext has not been initialized");
if (SynchronizationContext.Current != unityContext)
throw new NonUnityContextException();
}
/// <summary>
/// Execute a delegate on the Unity SynchronizationContext.
/// </summary>
/// <param name="action">Delegate to execute</param>
/// <returns>Task that completes when the work has been completed</returns>
public static Task ExecuteAsync(Action action)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (unityContext == null)
throw new InvalidOperationException("UnityContext has not been initialized");
if (SynchronizationContext.Current == unityContext)
{
try
{
action.Invoke();
return Task.CompletedTask;
}
catch (Exception e)
{
return Task.FromException(e);
}
}
else
{
var workHandle = new WorkHandle<object, object>(action, input: null);
unityContext.Post(WorkHandle<object, object>.Execute, workHandle);
return workHandle.WaitForComplete;
}
}
/// <summary>
/// Execute a delegate on the Unity SynchronizationContext.
/// </summary>
/// <param name="action">Delegate to execute</param>
/// <param name="input">Input to pass to the delegate</param>
/// <typeparam name="T">Type of the input data</typeparam>
/// <returns>Task that completes when the work has been completed</returns>
public static Task ExecuteAsync<T>(Action<T> action, T input)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
if (unityContext == null)
throw new InvalidOperationException("UnityContext has not been initialized");
if (SynchronizationContext.Current == unityContext)
{
try
{
action.Invoke(input);
return Task.CompletedTask;
}
catch (Exception e)
{
return Task.FromException(e);
}
}
else
{
var workHandle = new WorkHandle<T, object>(action, input);
unityContext.Post(WorkHandle<T, object>.Execute, workHandle);
return workHandle.WaitForComplete;
}
}
/// <summary>
/// Execute a delegate on the Unity SynchronizationContext and return the result.
/// </summary>
/// <param name="func">Delegate to execute</param>
/// <typeparam name="T">Type of the result data</typeparam>
/// <returns>Task that completes when the work has been completed</returns>
public static Task<T> ExecuteAsync<T>(Func<T> func)
{
if (func == null)
throw new ArgumentNullException(nameof(func));
if (unityContext == null)
throw new InvalidOperationException("UnityContext has not been initialized");
if (SynchronizationContext.Current == unityContext)
{
try
{
return Task.FromResult(func.Invoke());
}
catch (Exception e)
{
return Task.FromException<T>(e);
}
}
else
{
var workHandle = new WorkHandle<object, T>(func, input: null);
unityContext.Post(WorkHandle<object, T>.Execute, workHandle);
return workHandle.WaitForComplete;
}
}
/// <summary>
/// Execute a delegate on the Unity SynchronizationContext and return the result.
/// </summary>
/// <param name="func">Delegate to execute</param>
/// <param name="input">Input to pass to the delegate</param>
/// <typeparam name="TIn">Type of the input data</typeparam>
/// <typeparam name="TOut">Type of the result data</typeparam>
/// <returns>Task that completes when the work has been completed</returns>
public static Task<TOut> ExecuteAsync<TIn, TOut>(Func<TIn, TOut> func, TIn input)
{
if (func == null)
throw new ArgumentNullException(nameof(func));
if (unityContext == null)
throw new InvalidOperationException("UnityContext has not been initialized");
if (SynchronizationContext.Current == unityContext)
{
try
{
return Task.FromResult(func.Invoke(input));
}
catch (Exception e)
{
return Task.FromException<TOut>(e);
}
}
else
{
var workHandle = new WorkHandle<TIn, TOut>(func, input);
unityContext.Post(WorkHandle<TIn, TOut>.Execute, workHandle);
return workHandle.WaitForComplete;
}
}
}
public sealed class NonUnityContextException : Exception
{
public NonUnityContextException()
: base($"Invalid context: Has to be executed from the UnityThread")
{
}
}