Skip to content

Instantly share code, notes, and snippets.

@BastianBlokland
Last active July 9, 2019 17:15
Show Gist options
  • Save BastianBlokland/5e4a71ea76d72e246b01c18b9fb7a454 to your computer and use it in GitHub Desktop.
Save BastianBlokland/5e4a71ea76d72e246b01c18b9fb7a454 to your computer and use it in GitHub Desktop.
Utility to execute code on the unity-thread from background threads.

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")
    {
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment