TimeoutController uses PlayerLoopTimer(WIP, needs test)

master
neuecc 2021-03-12 13:01:46 +09:00
parent 62f6429b60
commit 49ca9364f7
3 changed files with 108 additions and 209 deletions

View File

@ -39,8 +39,6 @@ namespace Cysharp.Threading.Tasks
var trigger = gameObject.GetAsyncDestroyTrigger();
trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts);
}
}
}

View File

@ -27,23 +27,31 @@ namespace Cysharp.Threading.Tasks
this.state = state;
}
public static PlayerLoopTimer Create(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
public static PlayerLoopTimer Create(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
{
#if UNITY_EDITOR
// force use Realtime.
if (PlayerLoopHelper.IsMainThread && !UnityEditor.EditorApplication.isPlaying)
{
delayType = DelayType.Realtime;
}
#endif
switch (delayType)
{
case DelayType.UnscaledDeltaTime:
return new IgnoreTimeScalePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
return new IgnoreTimeScalePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
case DelayType.Realtime:
return new RealtimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
return new RealtimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
case DelayType.DeltaTime:
default:
return new DeltaTimePlayerLoopTimer(delayTimeSpan, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
return new DeltaTimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state);
}
}
public static PlayerLoopTimer StartNew(TimeSpan delayTimeSpan, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
public static PlayerLoopTimer StartNew(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
{
var timer = Create(delayTimeSpan, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state);
var timer = Create(interval, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state);
timer.Restart();
return timer;
}
@ -55,7 +63,19 @@ namespace Cysharp.Threading.Tasks
{
if (isDisposed) throw new ObjectDisposedException(null);
ResetCore(); // init state
ResetCore(null); // init state
isPlaying = true;
PlayerLoopHelper.AddAction(playerLoopTiming, this);
}
/// <summary>
/// Restart(Reset and Start) and change interval.
/// </summary>
public void Restart(TimeSpan interval)
{
if (isDisposed) throw new ObjectDisposedException(null);
ResetCore(interval); // init state
isPlaying = true;
PlayerLoopHelper.AddAction(playerLoopTiming, this);
}
@ -68,7 +88,7 @@ namespace Cysharp.Threading.Tasks
isPlaying = false;
}
protected abstract void ResetCore();
protected abstract void ResetCore(TimeSpan? newInterval);
public void Dispose()
{
@ -87,7 +107,7 @@ namespace Cysharp.Threading.Tasks
if (periodic)
{
ResetCore();
ResetCore(null);
return true;
}
else
@ -106,13 +126,12 @@ namespace Cysharp.Threading.Tasks
{
int initialFrame;
float elapsed;
readonly float delayTimeSpan;
float interval;
public DeltaTimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
public DeltaTimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
{
this.elapsed = 0.0f;
this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds;
ResetCore(interval);
this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1;
}
@ -127,7 +146,7 @@ namespace Cysharp.Threading.Tasks
}
elapsed += Time.deltaTime;
if (elapsed >= delayTimeSpan)
if (elapsed >= interval)
{
return false;
}
@ -135,9 +154,13 @@ namespace Cysharp.Threading.Tasks
return true;
}
protected override void ResetCore()
protected override void ResetCore(TimeSpan? interval)
{
elapsed = 0.0f;
this.elapsed = 0.0f;
if (interval != null)
{
this.interval = (float)interval.Value.TotalSeconds;
}
}
}
@ -145,13 +168,12 @@ namespace Cysharp.Threading.Tasks
{
int initialFrame;
float elapsed;
readonly float delayTimeSpan;
float interval;
public IgnoreTimeScalePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
public IgnoreTimeScalePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
{
this.elapsed = 0.0f;
this.delayTimeSpan = (float)delayTimeSpan.TotalSeconds;
ResetCore(interval);
this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1;
}
@ -166,7 +188,7 @@ namespace Cysharp.Threading.Tasks
}
elapsed += Time.unscaledDeltaTime;
if (elapsed >= delayTimeSpan)
if (elapsed >= interval)
{
return false;
}
@ -174,27 +196,30 @@ namespace Cysharp.Threading.Tasks
return true;
}
protected override void ResetCore()
protected override void ResetCore(TimeSpan? interval)
{
elapsed = 0.0f;
if (interval != null)
{
this.interval = (float)interval.Value.TotalSeconds;
}
}
}
sealed class RealtimePlayerLoopTimer : PlayerLoopTimer
{
ValueStopwatch stopwatch;
readonly long delayTimeSpanTicks;
long intervalTicks;
public RealtimePlayerLoopTimer(TimeSpan delayTimeSpan, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
public RealtimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action<object> timerCallback, object state)
: base(periodic, playerLoopTiming, cancellationToken, timerCallback, state)
{
this.stopwatch = ValueStopwatch.StartNew();
this.delayTimeSpanTicks = delayTimeSpan.Ticks;
ResetCore(interval);
}
protected override bool MoveNextCore()
{
if (stopwatch.ElapsedTicks >= delayTimeSpanTicks)
if (stopwatch.ElapsedTicks >= intervalTicks)
{
return false;
}
@ -202,9 +227,13 @@ namespace Cysharp.Threading.Tasks
return true;
}
protected override void ResetCore()
protected override void ResetCore(TimeSpan? interval)
{
this.stopwatch = ValueStopwatch.StartNew();
if (interval != null)
{
this.intervalTicks = interval.Value.Ticks;
}
}
}
}

View File

@ -1,8 +1,7 @@
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
using System.Threading;
using System;
using Cysharp.Threading.Tasks.Internal;
using System.Threading;
namespace Cysharp.Threading.Tasks
{
@ -14,26 +13,44 @@ namespace Cysharp.Threading.Tasks
public sealed class TimeoutController : IDisposable
{
readonly static Action<object> CancelCancellationTokenSourceStateDelegate = new Action<object>(CancelCancellationTokenSourceState);
static void CancelCancellationTokenSourceState(object state)
{
var cts = (CancellationTokenSource)state;
cts.Cancel();
}
CancellationTokenSource timeoutSource;
CancellationTokenSource linkedSource;
StoppableDelayRealtimePromise timeoutDelay;
PlayerLoopTimer timer;
bool isDisposed;
readonly DelayType delayType;
readonly PlayerLoopTiming delayTiming;
readonly CancellationTokenSource originalLinkCancellationTokenSource;
public TimeoutController()
public TimeoutController(DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update)
{
this.timeoutSource = new CancellationTokenSource();
this.originalLinkCancellationTokenSource = null;
this.linkedSource = null;
this.timeoutDelay = null;
this.delayType = delayType;
this.delayTiming = delayTiming;
}
public TimeoutController(CancellationTokenSource linkCancellationTokenSource)
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.timeoutDelay = null;
this.delayType = delayType;
this.delayTiming = delayTiming;
}
public CancellationToken Timeout(int millisecondsTimeout)
{
return Timeout(TimeSpan.FromMilliseconds(millisecondsTimeout));
}
public CancellationToken Timeout(TimeSpan timeout)
@ -43,6 +60,7 @@ namespace Cysharp.Threading.Tasks
return originalLinkCancellationTokenSource.Token;
}
// Timeouted, create new source and timer.
if (timeoutSource.IsCancellationRequested)
{
timeoutSource.Dispose();
@ -53,18 +71,26 @@ namespace Cysharp.Threading.Tasks
this.linkedSource.Dispose();
this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token);
}
timer?.Dispose();
timer = null;
}
if (timeoutDelay == null)
var useSource = (linkedSource != null) ? linkedSource : timeoutSource;
var token = useSource.Token;
if (timer == null)
{
RunDelayAsync(timeout).Forget(); // timeoutDelay = ... in RunDelayAsync(immediately, before await)
// Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled.
// (linked)token is canceled => stop timer
timer = PlayerLoopTimer.Create(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource);
}
else
{
timeoutDelay.RestartStopwatch(); // already running RunDelayAsync
timer.Restart(timeout);
}
return (linkedSource != null) ? linkedSource.Token : timeoutSource.Token;
return token;
}
public bool IsTimeout()
@ -74,184 +100,30 @@ namespace Cysharp.Threading.Tasks
public void Reset()
{
if (timeoutDelay != null)
{
timeoutDelay.Stop(); // stop delay, will finish RunDelayAsync
timeoutDelay = null;
}
}
async UniTaskVoid RunDelayAsync(TimeSpan timeout)
{
timeoutDelay = StoppableDelayRealtimePromise.Create(timeout, PlayerLoopTiming.Update, (linkedSource == null) ? CancellationToken.None : linkedSource.Token, out var version);
try
{
var reason = await new UniTask<DelayResult>(timeoutDelay, version);
if (reason == DelayResult.DelayCompleted)
{
// UnityEngine.Debug.Log("DEBUG:Timeout Complete, try to call timeoutSource.Cancel");
timeoutSource.Cancel();
}
else if (reason == DelayResult.LinkedTokenCanceled)
{
// UnityEngine.Debug.Log("DEBUG:LinkedSource IsCancellationRequested");
}
else if (reason == DelayResult.ExternalStopped)
{
// Reset(Promise.Stop) called, do nothing.
// UnityEngine.Debug.Log("DEBUG:Reset called");
}
}
finally
{
timeoutDelay = null;
}
timer.Stop();
}
public void Dispose()
{
if (timeoutDelay != null)
if (isDisposed) return;
try
{
timeoutDelay.Stop();
}
timeoutSource.Dispose();
if (linkedSource != null)
{
linkedSource.Dispose();
}
}
// stop timer.
timer.Dispose();
enum DelayResult
{
LinkedTokenCanceled,
ExternalStopped,
DelayCompleted, // as Timeout.
}
// Stop + SuppressCancellationThrow.
sealed class StoppableDelayRealtimePromise : IUniTaskSource<DelayResult>, IPlayerLoopItem, ITaskPoolNode<StoppableDelayRealtimePromise>
{
static OperationCanceledException ExterenalStopException = new OperationCanceledException();
static TaskPool<StoppableDelayRealtimePromise> pool;
StoppableDelayRealtimePromise nextNode;
public ref StoppableDelayRealtimePromise NextNode => ref nextNode;
static StoppableDelayRealtimePromise()
{
TaskPool.RegisterSizeGetter(typeof(StoppableDelayRealtimePromise), () => pool.Size);
}
long delayTimeSpanTicks;
ValueStopwatch stopwatch;
CancellationToken cancellationToken;
bool externalStop;
UniTaskCompletionSourceCore<DelayResult> core;
StoppableDelayRealtimePromise()
{
}
public static StoppableDelayRealtimePromise Create(TimeSpan delayTimeSpan, PlayerLoopTiming timing, CancellationToken cancellationToken, out short token)
{
if (!pool.TryPop(out var result))
// cancel and dispose.
timeoutSource.Cancel();
timeoutSource.Dispose();
if (linkedSource != null)
{
result = new StoppableDelayRealtimePromise();
}
result.stopwatch = ValueStopwatch.StartNew();
result.delayTimeSpanTicks = delayTimeSpan.Ticks;
result.cancellationToken = cancellationToken;
result.externalStop = false;
TaskTracker.TrackActiveTask(result, 3);
PlayerLoopHelper.AddAction(timing, result);
token = result.core.Version;
return result;
}
public void Stop()
{
externalStop = true;
}
public void RestartStopwatch()
{
stopwatch = ValueStopwatch.StartNew();
}
public DelayResult GetResult(short token)
{
try
{
return core.GetResult(token);
}
finally
{
TryReturn();
linkedSource.Cancel();
linkedSource.Dispose();
}
}
void IUniTaskSource.GetResult(short token)
finally
{
GetResult(token);
}
public UniTaskStatus GetStatus(short token)
{
return core.GetStatus(token);
}
public UniTaskStatus UnsafeGetStatus()
{
return core.UnsafeGetStatus();
}
public void OnCompleted(Action<object> continuation, object state, short token)
{
core.OnCompleted(continuation, state, token);
}
public bool MoveNext()
{
if (cancellationToken.IsCancellationRequested)
{
core.TrySetResult(DelayResult.LinkedTokenCanceled);
return false;
}
if (externalStop)
{
core.TrySetResult(DelayResult.ExternalStopped);
return false;
}
if (stopwatch.IsInvalid)
{
core.TrySetResult(DelayResult.DelayCompleted);
return false;
}
if (stopwatch.ElapsedTicks >= delayTimeSpanTicks)
{
core.TrySetResult(DelayResult.DelayCompleted);
return false;
}
return true;
}
bool TryReturn()
{
TaskTracker.RemoveTracking(this);
core.Reset();
stopwatch = default;
cancellationToken = default;
externalStop = false;
return pool.TryPush(this);
isDisposed = true;
}
}
}