Skip to content

Instantly share code, notes, and snippets.

@sebas77
Last active July 16, 2025 16:33
Show Gist options
  • Save sebas77/72dc1fb85345ebdf282663fb4925a086 to your computer and use it in GitHub Desktop.
Save sebas77/72dc1fb85345ebdf282663fb4925a086 to your computer and use it in GitHub Desktop.
How to hack UniTask to be able to stop task linked to a context when this is destroyed.
//Hack the method AddContinuation the file PlayerLoopHelper.cs
public static void AddContinuation(PlayerLoopTiming timing, Action continuation)
{
var context = SynchronizationContext.Current;
if (context != null && context != unitySynchronizationContext)
{
context.Post(_ => continuation(), null);
return;
}
var q = yielders[(int)timing];
if (q == null)
{
ThrowInvalidLoopTiming(timing);
}
q.Enqueue(continuation);
}
\\implement a SynchronizationContext like this one that will stop working when a cancellation token is triggered
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Threading;
using UnityEngine;
public sealed class TaskSynchronizationContext : SynchronizationContext
{
readonly CancellationToken _token;
public TaskSynchronizationContext(CancellationToken token, GameObject context)
{
_token = token;
context.GetOrAddComponent<PumpIt>().StartCoroutine(Pump());
}
readonly ConcurrentQueue<(SendOrPostCallback, object)> _execute = new();
readonly ConcurrentQueue<(SendOrPostCallback, object)> _wait = new();
public CancellationToken cancellationToken => _token;
// 1) Asynchronous continuations
public override void Post(SendOrPostCallback d, object state)
{
if (_token.IsCancellationRequested) return;
_wait.Enqueue((d, state)); // never run inline
}
IEnumerator Pump() // call once per frame
{
while (true)
{
while (_wait.TryDequeue(out var work)) // dequeue all the work items
{
_execute.Enqueue(work); // enqueue them for execution
}
// clear the wait queue
while (_execute.TryDequeue(out (SendOrPostCallback callback, object state) work))
if (_token.IsCancellationRequested == false)
{
var prev = Current;
try
{
SetSynchronizationContext(this);
work.callback(work.state);
}
finally
{
SetSynchronizationContext(prev);
}
}
else
{
Debug.LogWarning("TaskSynchronizationContext Pump cancelled, no more tasks will be processed.");
yield break; // exit the coroutine if cancellation is requested
}
yield return null; // wait for the next frame
}
}
// 2) Synchronous continuations (rare but they do occur)
public override void Send(SendOrPostCallback d, object state)
{
if (_token.IsCancellationRequested)
return;
d(state); // run inline on the current thread
}
// 3) Lifecycle of the *async method* itself
public override void OperationStarted()
{
if (_token.IsCancellationRequested)
throw new OperationCanceledException(_token);
base.OperationStarted(); // keep internal bookkeeping intact
}
// 4) Called when the async state-machine finishes
public override void OperationCompleted() => base.OperationCompleted();
// 5) Make sure the context flows across awaits
public override SynchronizationContext CreateCopy()
=> this;
}
// then it can be used like
var prev = SynchronizationContext.Current;
try
{
await UniTask.SwitchToSynchronizationContext(taskSynchronizationContext);
if (typeof(CLSignal).IsAssignableFrom(type))
{
success &= await ((CLSignal)signal).Run(taskSynchronizationContext.cancellationToken);
}
else if (typeof(CLSignal<W>).IsAssignableFrom(type))
success &= await ((CLSignal<W>)signal).Run(parameters, taskSynchronizationContext.cancellationToken);
}
finally
{
await UniTask.SwitchToSynchronizationContext(prev);
}
//all the tasks between the scope will use the new sync context (both Task and UniTask will respect that)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment