#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member using System; using System.Threading; namespace Cysharp.Threading.Tasks { // CancellationTokenSource itself can not reuse but CancelAfter(Timeout.InfiniteTimeSpan) allows reuse if did not reach timeout. // Similar discussion: // https://github.com/dotnet/runtime/issues/4694 // https://github.com/dotnet/runtime/issues/48492 // This TimeoutController emulate similar implementation, using CancelAfterSlim; to achieve zero allocation timeout. public sealed class TimeoutController : IDisposable { readonly static Action CancelCancellationTokenSourceStateDelegate = new Action(CancelCancellationTokenSourceState); static void CancelCancellationTokenSourceState(object state) { var cts = (CancellationTokenSource)state; cts.Cancel(); } CancellationTokenSource timeoutSource; CancellationTokenSource linkedSource; PlayerLoopTimer timer; bool isDisposed; readonly DelayType delayType; readonly PlayerLoopTiming delayTiming; readonly CancellationTokenSource originalLinkCancellationTokenSource; public TimeoutController(DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { this.timeoutSource = new CancellationTokenSource(); this.originalLinkCancellationTokenSource = null; this.linkedSource = null; this.delayType = delayType; this.delayTiming = delayTiming; } public TimeoutController(CancellationTokenSource linkCancellationTokenSource, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { this.timeoutSource = new CancellationTokenSource(); this.originalLinkCancellationTokenSource = linkCancellationTokenSource; this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, linkCancellationTokenSource.Token); this.delayType = delayType; this.delayTiming = delayTiming; } public CancellationToken Timeout(int millisecondsTimeout) { return Timeout(TimeSpan.FromMilliseconds(millisecondsTimeout)); } public CancellationToken Timeout(TimeSpan timeout) { if (originalLinkCancellationTokenSource != null && originalLinkCancellationTokenSource.IsCancellationRequested) { return originalLinkCancellationTokenSource.Token; } // Timeouted, create new source and timer. if (timeoutSource.IsCancellationRequested) { timeoutSource.Dispose(); timeoutSource = new CancellationTokenSource(); if (linkedSource != null) { this.linkedSource.Cancel(); this.linkedSource.Dispose(); this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token); } timer?.Dispose(); timer = null; } var useSource = (linkedSource != null) ? linkedSource : timeoutSource; var token = useSource.Token; if (timer == null) { // Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled. // (linked)token is canceled => stop timer timer = PlayerLoopTimer.StartNew(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); } else { timer.Restart(timeout); } return token; } public bool IsTimeout() { return timeoutSource.IsCancellationRequested; } public void Reset() { timer.Stop(); } public void Dispose() { if (isDisposed) return; try { // stop timer. timer.Dispose(); // cancel and dispose. timeoutSource.Cancel(); timeoutSource.Dispose(); if (linkedSource != null) { linkedSource.Cancel(); linkedSource.Dispose(); } } finally { isDisposed = true; } } } }