Last active
July 16, 2025 16:33
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
\\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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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