Donation ♥

×
If you're here, it's probably because you like my work and want to support me. Thank you in advance! All the support I get helps me immensely to continue doing what I do.

Thread Dispatcher


Thread Dispatcher is an open source tool to pass the execution of an Action, Func<TResult>, Coroutine, Task or Task<TResult> from a background thread to the main thread. You can determine the exact execution cycle during which a passed object it is executed and await its execution or result on the calling thread. Multiple features equip you with the tools for a wide range of situations in a multi-threaded environment.



Quick Start / TLDR


The core element of this asset is the Dispatcher class. Use this class to dispatch the execution of an Action, Func<TResult>, Coroutine, Task or Task<TResult> from a background thread to the main thread. Asynchronous overloads can be used to await the completion or the result of dispatched work on the calling thread and have full cancellation support. Use the optional ExecutionCycle argument to schedule the execution of the dispatched work for a specific update cycle (Update, FixedUpdate etc.).

// Task is running on a background thread.
public async Task WorkerTask()
{
// Dispatch an Action that is executed on the main thread.
Dispatcher.Invoke(() =>
{
// Executed on main thread.
});

// Dispatch an Action that is executed on the main thread and await its completion.
await Dispatcher.InvokeAsync(() =>
{
// Executed on main thread.
});
}

Technical Information


• Unity Version:   Unity 2019.4 (LTS) or newer
• Api Compatibility Level:   .NET Standard 2.0 or .NET 4.x
• Scripting Backend:   Mono or IL2CPP
• Last Update:   18.01.2022
• Asset Version:   2.0.1

Contact & Legal Information


• Author:   © 2022 Jonathan Lang
• Contact:   johnbaracudagames@gmail.com
• License:   MIT License

Actions


You can dispatch the execution of an Action to the main thread. Actions are presumably the most common type that will be dispatched and are by default executed during the next available Update, LateUpdate, FixedUpdate or Tick cycle. Use Dispatcher.Invoke(Action action, ExecutionCycle cycle,...) for more control over the cycle in which the Action will be executed.

public static void Invoke(Action action);
public static void Invoke(Action action, ExecutionCycle cycle);
// Tasks are executed on separate background threads.

public Task WorkerTask()
{
Dispatcher.Invoke(() =>
{
// Logic here is executed on the main thread.
});

return Task.CompletedTask;
}


public Task WorkerTaskCycle()
{
var context = ExecutionCycle.FixedUpdate;

Dispatcher.Invoke(() =>
{
// Logic here is executed on the main thread during the next FixedUpdate.
}, context);

return Task.CompletedTask;
}


Awaiting a dispatched Action


Await the completion of a dispatched Action by calling Dispatcher.InvokeAsync(Action action) which returns a Task that represents the execution of the dispatched action. The overload Dispatcher.InvokeAsync(Action action, ExecutionCycle cycle) can be used to determine the execution cycle to which the action should be dispatched to.

IMPORTANT NOTE! Exceptions thrown in a dispatched action are returned to the calling thread! If those exceptions are not handled within the passed action itself or on the calling thread, this will result in the thread being cancelled without notice.

public static Task InvokeAsync(Action action);
public static Task InvokeAsync(Action action, ExecutionCycle cycle);
// Tasks are executed on separate background threads.

public async Task WorkerTask()
{
// Create a try-catch block to handle potential exceptions.
try
{
// Dispatch and await the execution of an anonymous delegate.
await Dispatcher.InvokeAsync(() =>
{
// Logic here is executed on the main thread during the next Update or Tick call.
});

// Logic here is executed after the anonymous delegate has been executed on the main thread.
}
catch (Exception exception)
{
// Handle potential exceptions.
}
}


Cancellation of a dispatched Action


Every Dispatcher.InvokeAsync(Action action, ...) method returns a Task and has an optional overload that accepts a CancellationToken . Since actions return a non-generic task, an additional argument: throwOnCancellation can be passed, that determines whether an exception is thrown in the event of premature cancellation or not. By default, the value of this argument is true and an OperationCanceledException is thrown if the task is cancelled prematurely. If the value is set to false, no exception is thrown and the Task will return without notice.

Note that the throwOnCancellation argument will only prevent an OperationCanceledException from being thrown when the Task is canceled manually. Other exceptions that occur during the execution of the passed delegate are returned to the calling thread nonetheless.

public static Task InvokeAsync(Action action, CancellationToken ct, bool throwOnCancellation = true);
public static Task InvokeAsync(Action action, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);

// Tasks are executed on separate background threads.

public async Task WorkerTask(CancellationToken ct)
{
// Prepare a try catch block to catch the exception if the operation is cancelled.
try
{
// Dispatch and await the execution of an anonymous delegate.
// This will throw an exception if the operation is cancelled.
await Dispatcher.InvokeAsync(() =>
{
// Logic here is executed on the main thread during the next Update or Tick call.
}, ct);
}
catch (OperationCanceledException oce)
{
// Handle task cancellation.
}
catch (Exception exception)
{
// Handle other exceptions.
}


try
{
// Dispatch and await the execution of an anonymous delegate.
// If cancelled, this will return without an exception.
await Dispatcher.InvokeAsync(() =>
{
// ...
}, ct, throwOnCancellation: false);
}
catch (Exception exception)
{
// Handle exceptions.
}
}

Extension Methods for Actions


Multiple extension methods can be directly called on an Action to reduce boilerplate code. This naturally includes events.
Every overload of Dispatcher.Invoke(Action action,...) or Dispatcher.InvokeAsync(Action action,...) has an affiliated extension method. Extension methods for actions will also accept up to four generic parameters. This is done by boxing the generic action within the extension method. Extension methods are located at Baracuda.Threading.DispatchExtensions.

The amount of reduced boilerplate code is modes if you just want to dispatch an action without anything else. It will however become noticeable if you pass additional parameter to your action or if you need to perform a null-check first. Since we are calling the extension method on the action itself we can use the Null-conditional operator ?. to check if the action is null instead of creating a dedicated if statement.

public Task ActionExtensionMethodExampleA(Action action)
{
try
{
// Ordinary approach requires additional boilerplate code.
if (action != null)
{
Dispatcher.Invoke(action);
}

// Alternative extension method called directly on the Action.
action?.Dispatch();
}
catch (Exception exception)
{
// Handle exceptions.
}

return Task.CompletedTask;
}
public Task ActionExtensionMethodExampleB(Action<int> action)
{
try
{
// Ordinary approach requires additional boilerplate code.
// Note that we are using an alternative approach to check if the action is null.
Dispatcher.Invoke(() =>
{
action?.Invoke(1337);
});

// Alternative extension method called directly on the Action.
action?.Dispatch(1337);
}
catch (Exception exception)
{
// Handle exceptions.
}
return Task.CompletedTask;
}
public async Task ActionExtensionMethodExampleC(Action action, CancellationToken ct)
{
try
{
// Ordinary approach requires additional boilerplate code.
if (action != null)
{
await Dispatcher.InvokeAsync(action, ct);
}

// Alternative extension method called directly on the Action.
// Extension Method will return a completed task if the action is null.
await action.DispatchAsync(ct);
}
catch (OperationCanceledException oce)
{
// Handle exceptions.
}
catch (Exception exception)
{
// Handle exceptions.
}
}
public static void Dispatch(this Action action);
public static void Dispatch<T>(this Action<T> action);
public static void Dispatch<T, S>(this Action<T, S> action);
public static void Dispatch<T, S, U>(this Action<T, S, U> action);
public static void Dispatch<T, S, U, V>(this Action<T, S, U, V> action);

public static void Dispatch(this Action action, ExecutionCycle cycle);
public static void Dispatch<T>(this Action<T> action, ExecutionCycle cycle);
public static void Dispatch<T, S>(this Action<T, S> action, ExecutionCycle cycle);
public static void Dispatch<T, S, U>(this Action<T, S, U> action, ExecutionCycle cycle);
public static void Dispatch<T, S, U, V>(this Action<T, S, U, V> action, ExecutionCycle cycle);

public static Task DispatchAsync(this Action action);
public static Task DispatchAsync(this Action action, ExecutionCycle cycle);
public static Task DispatchAsync(this Action action, CancellationToken ct, bool throwOnCancellation = true);
public static Task DispatchAsync(this Action action, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);


Func<TResult>


Dispatch a Func<TResult> to the main thread and await its result on the calling thread using Dispatcher.InvokeAsync(Func<TResult> func). This method returns a Task<TResult> object which yields the result of the delegate. Pass in an optional ExecutionCycle argument when calling this method to determine when the delegate will be executed.

IMPORTANT NOTE! Exceptions thrown in a dispatched delegate are returned to the calling thread when awaited! If those exceptions are not handled within the passed delegate itself or on the calling thread, this will result in the thread being cancelled without notice.

public static Task<TResult> InvokeAsync(Func<TResult> func);
public static Task<TResult> InvokeAsync(Func<TResult> func, ExecutionCycle cycle);
public async Task WorkerTask()
{
try
{
// Find a CharacterController on the main thread and return it to the calling thread.
var player = await Dispatcher.InvokeAsync(() =>
{
return FindObjectOfType<CharacterController>();
});
}
catch (Exception exception)
{
// Handle potential exceptions.
}
}


Cancellation of a dispatched Func<TResult>


Every InvokeAsync(Func<TResult> func, ...) method returns a Task<TResult> and has an optional overload that accepts a CancellationToken. If the task is cancelled prematurely, an OperationCanceledException is thrown. Always use a try-catch block if you are passing a CancellationToken!

public static Task<TResult> InvokeAsync(Func<TResult> func, CancellationToken ct);
public static Task<TResult> InvokeAsync(Func<TResult> func, ExecutionCycle cycle, CancellationToken ct);
// Tasks are not executed on the main thread but on separate background threads.

public async Task WorkerTask(CancellationToken ct)
{
// Prepare a try catch block to catch the exception if the operation is cancelled.

try
{
// Dispatch and await the result of the passed delegate.
// This will throw an exception if the operation is cancelled.
Canvas result = await Dispatcher.InvokeAsync(() =>
{
// Find a Canvas on the main thread and return it to the calling thread.
return Object.FindObjectOfType<Canvas>();
}, ct);

// Executed logic that requires the Canvas...
}
catch (OperationCanceledException oce)
{
// Handle task cancellation.
}
catch (Exception exception)
{
// Handle other exceptions.
}
}


Extension Methods for Func<TResult>


You can call multiple extension methods directly on a Func<TResult> to reduce boilerplate code.

public async Task FuncExtensionMethodExampleA<T>(Func<T> func, CancellationToken ct)
{
try
{
// We must manually check if func is null because we cannot use the Null-conditional operator ?
// if we are dealing with tasks that yield a return value.
if (func != null)
{
// Ordinary approach requires some additional boilerplate code.
var result1 = await Dispatcher.InvokeAsync(func, ct);

// Alternative extension method called directly on the delegate.
var result2 = await func.DispatchAsync(ct);
}
}
catch (OperationCanceledException oce)
{
// Handle task cancellation.
}
catch (Exception exception)
{
// Handle exceptions.
}
}
public static Task<TResult> DispatchAsync(this Func<TResult> func);
public static Task<TResult> DispatchAsync(this Func<TResult> func, ExecutionCycle cycle);
public static Task<TResult> DispatchAsync(this Func<TResult> func, CancellationToken ct);
public static Task<TResult> DispatchAsync(this Func<TResult> func, ExecutionCycle cycle, CancellationToken ct);


Coroutines


You can dispatch an IEnumerator to be executed as a Coroutine on the main thread. You can determine the target MonoBehaviour on which the Coroutine will run. If no target MonoBehaviour is passed, the coroutine will run on the Dispatcher Scene Component.

public static void Invoke(IEnumerator enumerator);
public static void Invoke(IEnumerator enumerator, MonoBehaviour target);
public static void Invoke(IEnumerator enumerator, ExecutionCycle cycle);
public static void Invoke(IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target);
public Task WorkerTask()
{
Dispatcher.Invoke(ExampleCoroutine());

return Task.CompletedTask;
}


// Coroutines can only run on the main thread.
private IEnumerator ExampleCoroutine()
{
yield return null;
// ...
}

Awaiting a dispatched Coroutine


There are two options when awaiting a dispatched Coroutine. You can either Await the start of a Coroutine using InvokeAsyncAwaitStart which returns a Task<Coroutine> that when awaited will yield the representative Coroutine after it was successfully started on the main thread. Or Await the completion of a Coroutine using InvokeAsyncAwaitCompletion which yields no result but can be awaited indefinitely until the Coroutine has completed on the main thread.

NOTE! Version 2.0.0 enables you to determine whether the awaited Task returns when the dispatched Coroutine was started, yielding the Coroutine object as a return value or when it has completed, yielding no return value; by using InvokeAsyncAwaitStart(IEnumerator enumerator...) or InvokeAsyncAwaitCompletion(IEnumerator enumerator...) marking InvokeAsync(IEnumerator enumerator...) as Obsolete!
IMPORTANT NOTE! Exceptions thrown during both the dispatchment of a Coroutine or the execution of a Coroutine are returned to the calling thread. If those exceptions are not handled on the calling thread, the thread will be cancelled without notice.


Await the start of a Coroutine


The method Dispatcher.InvokeAsyncAwaitStart(IEnumerator enumerator,...) returns a Task that when awaited yields the representative Coroutine object that was just started on the main thread. You can cache this object and use it to stop the Coroutine using Dispatcher.CancelCoroutine(Coroutine coroutine,...), a method which also has asynchronous overloads and will stop a Coroutine running on the dispatcher itself. Note that this method will only stop Coroutines that are running on the dispatcher itself, aka Coroutines that were dispatched without explicitly passing a target MonoBehaviour for the Coroutine .

public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator);
public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, MonoBehaviour target);
public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, CancellationToken ct);
public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, MonoBehaviour target, CancellationToken ct);

public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, ExecutionCycle cycle);
public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target);
public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, ExecutionCycle cycle, CancellationToken ct);
public static Task<Coroutine> InvokeAsyncAwaitStart(IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target, CancellationToken ct);
// Task is running on a background thread.
public async Task WorkerTask()
{
try
{
// Get a random value of milliseconds to wait.
var random = await Dispatcher.InvokeAsync(() => UnityEngine.Random.Range(0, 2000));

// Start a coroutine running on the main thread.
Coroutine coroutine = await Dispatcher.InvokeAsyncAwaitStart(ExampleCoroutine());

// Simulate asynchronous work.
await Task.Delay(random);

// Cancel the coroutine after asynchronous work has completed.
await Dispatcher.CancelCoroutineAsync(coroutine);

// ...
}
catch (Exception exception)
{
// Handle potential exceptions.
Debug.LogException(exception);
}
}

// Coroutine has a ~50% chance of being stopped before it completes.
private IEnumerator ExampleCoroutine()
{
Debug.Log("Start of Coroutine");
yield return new WaitForSeconds(1f);
Debug.Log("End of Coroutine");
}

Await the completion of a Coroutine


You can not only await the start of a Coroutine but also its completion. The method Dispatcher.InvokeAsyncAwaitCompletion(IEnumerator enumerator,...) returns a Task that can be awaited on the calling thread and returns when the dispatched Coroutine has completed on the main thread. To avoid that the calling thread is awaiting the completion indefinitely, it is very important to receive notification if the Coroutine cannot complete, either because of an exception or because the coroutine was stopped. For this reason every dispatched coroutine must be wrapped in another exception sensitive coroutine, that will catch and return exceptions to the calling thread. Additionally, because the life of a Coroutine is bound to a target MonoBehaviour and a stopped Coroutine will just cease to exist without telling anybody, the target MonoBehaviour must be monitored to receive notice if it is disabled. Those essential operations can become very expensive should they occur in large quantities.

NOTE! For reasons stated above, awaiting the completion of a dispatched coroutine can become an expensive operation should it occur in large quantities. Please be aware of this and avoid unnecessary usages of this feature if possible, especially in performance critical environments.

public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, bool throwExceptions = true);
public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, CancellationToken ct, bool throwExceptions = true);
public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, MonoBehaviour target, bool throwExceptions = true);
public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, MonoBehaviour target, CancellationToken ct, bool throwExceptions = true);

public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, ExecutionCycle cycle, bool throwExceptions = true);
public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, ExecutionCycle cycle, CancellationToken ct, bool throwExceptions = true);
public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target, bool throwExceptions = true);
public static Task InvokeAsyncAwaitCompletion(IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target, CancellationToken ct, bool throwExceptions = true);
public async Task WorkerTask()
{
try
{
// Create a cancellation token source.
var cts = new CancellationTokenSource();

// Cancel the operation after one second.
cts.CancelAfter(1000);

// Start a coroutine and await its completion.
await Dispatcher.InvokeAsyncAwaitCompletion(ExampleCoroutine(), cts.Token);

// ...
}
catch (OperationCanceledException operationCanceledException)
{
// This exception will occur after one second if the coroutine hasn't completed yet, which has a chance of ~50%.
Debug.Log("Operation Cancelled!");
}
catch (BehaviourDisabledException behaviourDisabledException)
{
// This exception will occur if the coroutines target behaviour is disabled while the coroutine is still running.
Debug.Log("Behaviour disabled!");
}
catch (Exception exception)
{
// Handle other potential exceptions that occur during the execution of the coroutine.
Debug.LogError(exception);
}
}

// Coroutines are only allowed to be executed on the main thread.
private IEnumerator ExampleCoroutine()
{
Debug.Log("Start of Coroutine");
yield return new WaitForSeconds(UnityEngine.Random.Range(0f, 2f));
Debug.Log("End of Coroutine");
}

Extension Methods for Coroutines


You can call multiple extension methods directly on an IEnumerator to reduce boilerplate code. Every overload of Dispatcher.Invoke(IEnumerator enumerator,...) or Dispatcher.InvokeAsync(IEnumerator enumerator,...) has an affiliated extension method.

public async Task CoroutineExtensionMethodExampleA(MonoBehaviour target)
{
try
{
// Ordinary approach to dispatch a coroutine.
Dispatcher.Invoke(ExampleCoroutine(5.0f), target);

// Alternative extension method.
ExampleCoroutine(5.0f).Dispatch(target);
}
catch (Exception exception)
{
// Handle potential exceptions.
}
}

private IEnumerator ExampleCoroutine(float delay)
{
yield return new WaitForSeconds(delay);
}
public static void Dispatch(this IEnumerator enumerator);
public static void Dispatch(this IEnumerator enumerator, MonoBehaviour target);
public static void Dispatch(this IEnumerator enumerator, ExecutionCycle cycle);
public static void Dispatch(this IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target);

public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator);
public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, MonoBehaviour target);
public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, CancellationToken ct);
public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, MonoBehaviour target, CancellationToken ct);

public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, ExecutionCycle cycle);
public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target);
public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, ExecutionCycle cycle, CancellationToken ct);
public static Task<Coroutine> DispatchAsyncAwaitStart(this IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target, CancellationToken ct);

public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, bool throwExceptions = true);
public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, MonoBehaviour target, bool throwExceptions = true);
public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, CancellationToken ct, bool throwExceptions = true);
public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, MonoBehaviour target, CancellationToken ct, bool throwExceptions = true);

public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, ExecutionCycle cycle, bool throwExceptions = true);
public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target, bool throwExceptions = true);
public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, ExecutionCycle cycle, CancellationToken ct, bool throwExceptions = true);
public static Task DispatchAsyncAwaitCompletion(this IEnumerator enumerator, ExecutionCycle cycle, MonoBehaviour target, CancellationToken ct, bool throwExceptions = true);


Task


Dispatch work encapsulated in a Func<Task> which is then preformed on the main thread. When passing in an optional CancellationToken ct, it is forwarded to the Task wrapped in function which must accept it as an argument.

public static void Invoke(Func<Task> function);
public static void Invoke(Func<Task> function, ExecutionCycle cycle);
public static void Invoke(Func<CancellationToken, Task> function, CancellationToken ct, bool throwOnCancellation = true);
public static void Invoke(Func<CancellationToken, Task> function, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);
// Provides cancellation token.
private CancellationTokenSource cts = new CancellationTokenSource();

// Running on a background thread.
public Task WorkerTask()
{
// The cancellation token from cts is passed along to MainThreadTask
// when it is run on the main thread.
Dispatcher.Invoke(MainThreadTask, cts.Token);

// using cts.Cancel() will cancel the work done by MainThreadTask.
}


// Running on the main thread.
private async Task MainThreadTask(CancellationToken ct)
{
// Simulating async work on the main thread and returning a result.
await Task.Delay(300, ct);
}



Awaiting a dispatched Task


Dispatch work encapsulated in a Func<Task> which is then preformed on the main thread. Await the completion of the passed operation by awaiting the Task handle returned by the method call. When passing in an optional CancellationToken ct, it is forwarded to the Task wrapped in function which must accept it as an argument.

IMPORTANT NOTE! Exceptions thrown in a dispatched task are returned to the calling thread if the completion or the result of the dispatched work is awaited! If those exceptions are not handled within the passed task itself this will result in the thread being cancelled without notice.

public static Task InvokeAsync(Func<Task> function);
public static Task InvokeAsync(Func<Task> function, ExecutionCycle cycle);
public static Task InvokeAsync(Func<CancellationToken, Task> function, CancellationToken ct, bool throwOnCancellation = true);
public static Task InvokeAsync(Func<CancellationToken, Task> function, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);
// Running on a background thread.
public async Task WorkerTask(CancellationToken ct)
{
try
{
// Perform and await work on the main thread.
await Dispatcher.InvokeAsync(MainThreadTask, cts.Token);

// ...
}
catch (OperationCanceledException canceledException)
{
// Task was canceled.
}
catch (Exception exception)
{
// Another exception occurred.
}
}


// Running on the main thread.
private async Task MainThreadTask(CancellationToken ct)
{
// Simulating async work on the main thread.
await Task.Delay(300, ct);
}



Awaiting a dispatched Task<TResult>


Dispatch work encapsulated in a Func<Task<TResult>> which is then preformed on the main thread. Await the result of the passed operation by awaiting the Task<TResult> handle returned by the method call. When passing in an optional CancellationToken ct, it is forwarded to the Task wrapped in function which must accept it as an argument.

IMPORTANT NOTE! Exceptions thrown in a dispatched task are returned to the calling thread if the completion or the result of the dispatched work is awaited! If those exceptions are not handled within the passed task itself this will result in the thread being cancelled without notice.

public static Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function);
public static Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> function, ExecutionCycle cycle);
public static Task<TResult> InvokeAsync<TResult>(Func<CancellationToken, Task<TResult>> function, CancellationToken ct, bool throwOnCancellation = true);
public static Task<TResult> InvokeAsync<TResult>(Func<CancellationToken, Task<TResult>> function, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);
// Running on a background thread.
public async Task WorkerTask(CancellationToken ct)
{
try
{
// Perform and await work on the main thread.
int result = await Dispatcher.InvokeAsync(MainThreadTask, cts.Token);

// Do something with result.
}
catch (OperationCanceledException canceledException)
{
// Task was canceled.
}
catch (Exception exception)
{
// Another exception occurred.
}
}


// Running on the main thread.
private async Task<int> MainThreadTask(CancellationToken ct)
{
// Simulating async work on the main thread and return a result.
await Task.Delay(300, ct);
return 1337;
}


Extension Methods for Task and Task<TResult>

public static void Invoke(this Func<Task> function);
public static void Invoke(this Func<Task> function, ExecutionCycle cycle);
public static void Invoke(this Func<CancellationToken, Task> function, CancellationToken ct, bool throwOnCancellation = true);
public static void Invoke(this Func<CancellationToken, Task> function, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);

public static Task InvokeAsync(this Func<Task> function);
public static Task InvokeAsync(this Func<Task> function, ExecutionCycle cycle);
public static Task InvokeAsync(this Func<CancellationToken, Task> function, CancellationToken ct, bool throwOnCancellation = true);
public static Task InvokeAsync(this Func<CancellationToken, Task> function, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);

public static Task<TResult> InvokeAsync<TResult>(this Func<Task<TResult>> function);
public static Task<TResult> InvokeAsync<TResult>(this Func<Task<TResult>> function, ExecutionCycle cycle);
public static Task<TResult> InvokeAsync<TResult>(this Func<CancellationToken, Task<TResult>> function, CancellationToken ct, bool throwOnCancellation = true);
public static Task<TResult> InvokeAsync<TResult>(this Func<CancellationToken, Task<TResult>> function, ExecutionCycle cycle, CancellationToken ct, bool throwOnCancellation = true);



MEC Coroutines (Experimental)


NOTE! MEC coroutine support is currently in an experimental state. The state of the asset and the documentation regarding this feature might contain errors, and or be incomplete.
This section is about support for the third party plugin More Effective Coroutines. If you have at least the free version of More Effective Coroutines (MEC) setup in your project and want to use the Dispatcher to be able to pass the execution of MEC Coroutines to the main thread, you have to enable this feature with some manual work by following these steps:

• Make sure that MEC is located in its own assembly. You can do this by creating an Assembly Definition File (ADF) in the root folder that contains MEC.
• Reference the newly created MEC assembly in the dispatchers ADF located at Baracuda/Threading/Assembly-Baracuda-Threading.asmdef.
• Make sure that the contents for MEC are located in their native namespace MEC.
• Define the symbol EXPERIMENTAL_ENABLE_MEC.
• You should now be able to use this feature. MEC integration can be found at Baracuda/Threading/Dispatcher.MEC.cs. (for debugging)

If you've enabled this feature you can now use the following methods to dispatch MEC coroutines and to await their start / completion as needed.


IMPORTANT NOTE! MEC Coroutines are not exception sensitive meaning that if you await the completion of a MEC coroutine and an exception occurs within the coroutine, neither the exception nor the coroutine will return to the calling thread blocking it indefinitely.
public static void InvokeMecCoroutine(IEnumerator<float> enumerator);
public static void InvokeMecCoroutine(IEnumerator<float> enumerator, ExecutionCycle cycle);

public static Task<CoroutineHandle> RunMecCoroutineAsyncAwaitStart(IEnumerator<float> enumerator);
public static Task<CoroutineHandle> RunMecCoroutineAsyncAwaitStart(IEnumerator<float> enumerator, ExecutionCycle cycle);
public static Task<CoroutineHandle> RunMecCoroutineAsyncAwaitStart(IEnumerator<float> enumerator, CancellationToken ct);
public static Task<CoroutineHandle> RunMecCoroutineAsyncAwaitStart(IEnumerator<float> enumerator, ExecutionCycle cycle, CancellationToken ct);

public static Task RunMecCoroutineAsyncAwaitCompletion(IEnumerator<float> enumerator);
public static Task RunMecCoroutineAsyncAwaitCompletion(IEnumerator<float> enumerator, ExecutionCycle cycle);
public static Task RunMecCoroutineAsyncAwaitCompletion(IEnumerator<float> enumerator, CancellationToken ct);
public static Task RunMecCoroutineAsyncAwaitCompletion(IEnumerator<float> enumerator, ExecutionCycle cycle, CancellationToken ct);

Execution Cycle


You can determine the exact execution cycle in which a passed delegate or coroutine is invoked on the main thread by passing an optional ExecutionCycle argument when dispatching it. Every one of the available execution cycles can be completely disabled using custom preprocessor symbols.

public enum ExecutionCycle
{
Default = TickUpdate, // Default may change depending on enabled execution cycles.

Update = 1,
LateUpdate = 2,
PostUpdate = 3,
FixedUpdate = 4,
TickUpdate = 5,
EditorUpdate = 6,
}


Update, LateUpdate and FixedUpdate all represent their corresponding Unity execution cycle and are called before the default execution time. PostUpdate is technically another LateUpdate call, that is invoked after the default execution time. TickUpdate is a custom execution cycle that runs during Edit- and Playmode and is called in intervals of 50 milliseconds. EditorUpdate is only called in the Unity editor and will run during both edit and playmode.

If the ExecutionCycle is not explicitly specified, the execution will happen during the next available execution cycle. This is not the same as passing ExecutionCycle.Default which will prefer a certain cycle, depending on the currently enabled cycles. Default will prefer execution cycles in the following order: TickUpdate > Update > LateUpdate > FixedUpdate > EditorUpdate. This means that Default will always correspond to TickUpdate if it is enabled. If however TickUpdate is disabled, Default will correspond to Update and so on.

Demonstration:

If you copy and execute the following example code, your console output should look similar to that of the image you can find below. Be aware that this will only work if DISPATCHER_DEBUG is defined and if all execution cycles are enabled.

// Execute this task on a background thread.
public static Task ExecutionCycleOrderExample()
{
// Dispatcher.CurrentCycle is only available if DISPATCHER_DEBUG is defined.
#if DISPATCHER_DEBUG

// Executed during the next available Update call.
Dispatcher.Invoke(() => Debug.Log($"Update: {Dispatcher.CurrentCycle}"), ExecutionCycle.Update);

// Executed during the next available LateUpdate call.
// Called before every other (default time) LateUpdate.
Dispatcher.Invoke(() => Debug.Log($"LateUpdate: {Dispatcher.CurrentCycle}"), ExecutionCycle.LateUpdate);

// Executed during the next available PostUpdate call.
// Called after every other (default time) LateUpdate.
Dispatcher.Invoke(() => Debug.Log($"PostUpdate: {Dispatcher.CurrentCycle}"), ExecutionCycle.PostUpdate);

// Executed during the next available FixedUpdate call.
Dispatcher.Invoke(() => Debug.Log($"FixedUpdate: {Dispatcher.CurrentCycle}"), ExecutionCycle.FixedUpdate);

// Executed during the next available TickUpdate call.
// Called every 50 milliseconds.
Dispatcher.Invoke(() => Debug.Log($"TickUpdate: {Dispatcher.CurrentCycle}"), ExecutionCycle.TickUpdate);

// Executed during the next available EditorUpdate call.
// Should only be used in editor scripts.
Dispatcher.Invoke(() => Debug.Log($"EditorUpdate: {Dispatcher.CurrentCycle}"), ExecutionCycle.EditorUpdate);


// ...
Dispatcher.Invoke(() => Debug.Log($"Default: {Dispatcher.CurrentCycle}"), ExecutionCycle.Default);

// ...
Dispatcher.Invoke(() => Debug.Log($"Undefined: {Dispatcher.CurrentCycle}"));

#endif // DISPATCHER_DEBUG

return Task.CompletedTask;
}

Resulting console output:

Demonstration: approximate probability of execution

If you copy and execute the next code example you can illustrate with what likelihood each execution cycle is approximately used, if it was not explicitly specified. Be aware that this will drastically vary for each project.

[RuntimeInitializeOnLoadMethod]
public static void IllustrateDispatchedExecutions()
{
#if DISPATCHER_DEBUG

Task.Run(async () =>
{
for (var i = 0; i < 100000; i++)
{
await Dispatcher.InvokeAsync(() =>
{
Debug.Log($"Dispatched During: {Dispatcher.CurrentCycle}");
});
}
});

#endif // DISPATCHER_DEBUG
}
Resulting console output:

Preprocessor Symbols


Thread Dispatcher includes multiple custom preprocessor symbols that can be used to enable and disable whole modules of this asset. I would recommend you one of my other (free) assets: Preprocessor Symbol Definition Files to handle those symbols in an easy and intuitive way.


Every execution cycle can be completely enabled and disabled by defining a custom preprocessor symbol. The purpose of this feature is to reduce overhead from unnecessary event methods and custom execution cycles that are not required. If every one of those execution cycles is disabled TickUpdate will be re-enabled as a fallback.


DISPATCHER_DISABLE_UPDATE
Disable the Unity Update cycle for the Dispatcher.

DISPATCHER_DISABLE_LATEUPDATE
Disable the Unity LateUpdate cycle for the Dispatcher.

DISPATCHER_DISABLE_POSTUPDATE
Disable the custom PostUpdate cycle for the Dispatcher.

DISPATCHER_DISABLE_FIXEDUPDATE
Disable the Unity FixedUpdate cycle for the Dispatcher.

DISPATCHER_DISABLE_TICKUPDATE
Disable the custom TickUpdate cycle for the Dispatcher.

DISPATCHER_DISABLE_EDITORUPDATE
Disable the Unity EditorUpdate cycle for the Dispatcher.
DISPATCHER_DISABLE_UPDATE   Disable the Unity Update cycle for the Dispatcher.  
DISPATCHER_DISABLE_LATEUPDATE   Disable the Unity LateUpdate cycle for the Dispatcher.  
DISPATCHER_DISABLE_POSTUPDATE   Disable the custom PostUpdate cycle for the Dispatcher.  
DISPATCHER_DISABLE_FIXEDUPDATE   Disable the Unity FixedUpdate cycle for the Dispatcher.  
DISPATCHER_DISABLE_TICKUPDATE   Disable the custom TickUpdate cycle for the Dispatcher.  
DISPATCHER_DISABLE_EDITORUPDATE   Disable the Unity EditorUpdate cycle for the Dispatcher.  

You can also define the optional symbol DISPATCHER_DEBUG which will enable multiple additional warning logs and allow you access to the property Dispatcher.CurrentCycle to get the ExecutionCycle definition of the currently executed execution cycle.


The image below shows how the Preprocessor Symbol Definition Files asset can be used in combination with this asset.

Miscellaneous


IsMainThread()


You can use the Dispatcher to validate if a method is currently running on the main thread or not by calling Dispatcher.IsMainThread(). This method will return true if it is called from the main thread.

public static bool IsMainThread();
// It is unknown if the task is executed on the main thread or not.

public Task WorkerTask()
{
if(Dispatcher.IsMainThread() == true)
{
// Work() can be called directly because the current execution is already happening on the main thread.
Work();
}
else
{
// Work() must first be dispatched to the main thread and will be called during the next available
// Update() or Tick() cycle.
Dispatcher.Invoke(Work);
}

return Task.CompletedTask;
}

private void Work()
{
// Logic here is only allowed to be executed on the main thread!
}

Validation


You can manually validate that a dispatcher instance exists, by calling Dispatcher.Validate() This Method is a wrapper for the property Dispatcher.Current which is guaranteed to return an active instance. Note that there is no common reason to call this method. It should only be used if it is unclear if the scene object/component is or was destroyed during runtime.

public static Dispatcher Validate();

Current Execution Cycle


You can get the ExecutionCycle definition of the currently executed execution cycle by calling Dispatcher.CurrentCycle. This property is only available if DISPATCHER_DEBUG is defined.

public static ExecutionCycle CurrentCycle;

Scene Component


The Dispatcher requires a scene component in order to intercept unity event messages. The Scene Component will be created and validated automatically. If you want to validate and instantiate the scene component manually, navigate to (menu: Tools > Dispatcher > Validate Scene Component) or do so by adding the Dispatcher.cs component to an empty GameObject. Only one component is allowed to exist at any point. If multiple components are found, every component except one will be destroyed. Please ensure that the Scene Component or its GameObject are not accidentally destroyed during runtime!



Script Execution Order


You can set two different Script Execution Order values for the Dispatcher. First the Main Execution Order which will affect its Update, LateUpdate and FixedUpdate calls and second the Post Update Order which only affects its PostUpdate call. Post Update is technically another LateUpdate call that is invoked on another component and then forwarded to the Dispatcher.


If you have to alter the dispatchers script execution order navigate to: (menu: Tools > Dispatcher > Script Execution Order) I strongly recommend keeping the Main Execution Order below 0 (default execution time) and the Post Update Order greater than 0 (default execution time). I would also recommend keeping the Main Execution Order below the Post Update Order.


IDisableCallback (interface)


You can implement this interface in a MonoBehaviour that is passed as a target when dispatching and awaiting a coroutine. This interface is then used by the dispatcher to receive a callback if the target behaviour was disabled while the coroutine is still running, so it is important that the onDisable event is invoked if the target behaviour is disabled (OnDisable). If you are not passing a target MonoBehaviour when dispatching a Coroutine (which I would advise), the dispatcher itself will act as the target for the coroutine which already implements this interface and should not be disabled during runtime anyway.

Dispatcher.RuntimeToken


Dispatcher.RuntimeToken returns a CancellationToken that is valid for the duration of the applications runtime. This means until OnApplicationQuit is called in a build or until the play state is changed in the editor. You can pass this token along when creating a background thread to ensure that the thread is killed when exiting the game.

public static CancellationToken RuntimeToken;

FAQ


I found a bug!

Contact me directly via Mail or use my socials. Feel free to create an issue at the GitHub Repository.


How can I support you?

I'm happy about every form of support I get. A good review on the Asset Store, exposure on my socials: Twitter, GitHub, Itch or a donation ♥


I have another question!

Contact me: Mail, Twitter.