534 lines
16 KiB
C#
534 lines
16 KiB
C#
using System;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AMWD.Common.Utilities
|
|
{
|
|
// originally inspired by a code from Yves Goergen (unclassified.software).
|
|
|
|
/// <summary>
|
|
/// Implements an awaitable task that runs after a specified delay. The delay can be reset.
|
|
/// By resetting the delay, the task can be executed multiple times.
|
|
/// </summary>
|
|
public class DelayedTask
|
|
{
|
|
#region Data
|
|
|
|
private Timer _timer;
|
|
|
|
private bool _nextRunPending;
|
|
|
|
/// <summary>
|
|
/// The synchronisation object.
|
|
/// </summary>
|
|
protected readonly object syncLock = new();
|
|
|
|
/// <summary>
|
|
/// The exception handler.
|
|
/// </summary>
|
|
protected Action<Exception> exceptionHandler;
|
|
|
|
/// <summary>
|
|
/// Provides the <see cref="Task"/> for the <see cref="GetAwaiter"/> method.
|
|
/// </summary>
|
|
protected TaskCompletionSourceWrapper tcs;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the action to execute.
|
|
/// </summary>
|
|
protected Action Action { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the timer is running and an execution is scheduled. This
|
|
/// is mutually exclusive to <see cref="IsRunning"/>.
|
|
/// </summary>
|
|
public bool IsWaitingToRun { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the action is currently running. This is mutually
|
|
/// exclusive to <see cref="IsWaitingToRun"/>.
|
|
/// </summary>
|
|
public bool IsRunning { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the delay to wait before executing the action.
|
|
/// </summary>
|
|
public TimeSpan Delay { get; protected set; }
|
|
|
|
#endregion Data
|
|
|
|
#region Static methods
|
|
|
|
/// <summary>
|
|
/// Creates a new task instance that executes the specified action after the delay, but does
|
|
/// not start it yet.
|
|
/// </summary>
|
|
/// <param name="action">The action to execute.</param>
|
|
/// <param name="delay">The delay.</param>
|
|
/// <returns></returns>
|
|
public static DelayedTask Create(Action action, TimeSpan delay)
|
|
=> new() { Action = action, Delay = delay };
|
|
|
|
/// <summary>
|
|
/// Creates a new task instance that executes the specified action after the delay, but does
|
|
/// not start it yet.
|
|
/// </summary>
|
|
/// <param name="action">The action to execute.</param>
|
|
/// <param name="delay">The delay.</param>
|
|
/// <returns></returns>
|
|
public static DelayedTaskWithResult<TResult> Create<TResult>(Func<TResult> action, TimeSpan delay)
|
|
=> DelayedTaskWithResult<TResult>.Create(action, delay);
|
|
|
|
/// <summary>
|
|
/// Executes the specified action after the delay.
|
|
/// </summary>
|
|
/// <param name="action">The action to execute.</param>
|
|
/// <param name="delay">The delay.</param>
|
|
/// <returns></returns>
|
|
public static DelayedTask Run(Action action, TimeSpan delay)
|
|
=> new DelayedTask { Action = action, Delay = delay }.Start();
|
|
|
|
/// <summary>
|
|
/// Executes the specified action after the delay.
|
|
/// </summary>
|
|
/// <param name="action">The action to execute.</param>
|
|
/// <param name="delay">The delay.</param>
|
|
/// <returns></returns>
|
|
public static DelayedTaskWithResult<TResult> Run<TResult>(Func<TResult> action, TimeSpan delay)
|
|
=> DelayedTaskWithResult<TResult>.Run(action, delay);
|
|
|
|
#endregion Static methods
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="DelayedTask"/> class.
|
|
/// </summary>
|
|
protected DelayedTask()
|
|
{
|
|
tcs = CreateTcs();
|
|
SetLastResult(tcs);
|
|
}
|
|
|
|
#endregion Constructors
|
|
|
|
#region Public instance methods
|
|
|
|
/// <summary>
|
|
/// Resets the delay and restarts the timer. If an execution is currently pending, it is
|
|
/// postponed until the full delay has elapsed again. If no execution is pending, the action
|
|
/// will be executed again after the delay.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
if (!IsWaitingToRun && !IsRunning)
|
|
{
|
|
// Let callers wait for the next execution
|
|
tcs = CreateTcs();
|
|
}
|
|
IsWaitingToRun = true;
|
|
if (_timer != null)
|
|
{
|
|
_timer.Change(Delay, Timeout.InfiniteTimeSpan);
|
|
}
|
|
else
|
|
{
|
|
_timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels the delay. Any pending execution is cleared. If the action was pending but not
|
|
/// yet executing, this task is cancelled. If the action was not pending or is already
|
|
/// executing, this task will be completed successfully after the action has completed.
|
|
/// </summary>
|
|
public void Cancel()
|
|
{
|
|
TaskCompletionSourceWrapper localTcs = null;
|
|
lock (syncLock)
|
|
{
|
|
IsWaitingToRun = false;
|
|
_nextRunPending = false;
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
if (!IsRunning)
|
|
{
|
|
localTcs = tcs;
|
|
}
|
|
}
|
|
|
|
// Complete the task (as cancelled) so that nobody needs to wait for an execution that
|
|
// isn't currently scheduled
|
|
localTcs?.TrySetCanceled();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a pending execution immediately, not waiting for the timer to elapse.
|
|
/// </summary>
|
|
/// <returns><c>true</c>, if an execution was started; otherwise, <c>false</c>.</returns>
|
|
public bool ExecutePending()
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
if (!IsWaitingToRun && !IsRunning)
|
|
{
|
|
return false;
|
|
}
|
|
IsWaitingToRun = true;
|
|
if (_timer != null)
|
|
{
|
|
_timer.Change(TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
|
}
|
|
else
|
|
{
|
|
_timer = new Timer(OnTimerCallback, null, TimeSpan.Zero, Timeout.InfiniteTimeSpan);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an awaiter used to await this <see cref="DelayedTask"/>.
|
|
/// </summary>
|
|
/// <returns>An awaiter instance.</returns>
|
|
public TaskAwaiter GetAwaiter()
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
return tcs.Task.GetAwaiter();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="System.Threading.Tasks.Task"/> that represents the current awaitable
|
|
/// operation.
|
|
/// </summary>
|
|
public Task Task
|
|
{
|
|
get
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
return tcs.Task;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an implicit conversion from <see cref="DelayedTask"/> to
|
|
/// <see cref="System.Threading.Tasks.Task"/>.
|
|
/// </summary>
|
|
/// <param name="delayedTask">The <see cref="DelayedTask"/> instance to cast.</param>
|
|
/// <returns>A <see cref="System.Threading.Tasks.Task"/> that represents the current
|
|
/// awaitable operation.</returns>
|
|
public static implicit operator Task(DelayedTask delayedTask) => delayedTask.Task;
|
|
|
|
/// <summary>
|
|
/// Gets the exception of the last execution. If the action has not yet thrown any
|
|
/// exceptions, this will return null.
|
|
/// </summary>
|
|
public Exception Exception => Task.Exception;
|
|
|
|
/// <summary>
|
|
/// Adds an unhandled exception handler to this <see cref="DelayedTask"/> instance.
|
|
/// </summary>
|
|
/// <param name="exceptionHandler">The action that handles an exception.</param>
|
|
/// <returns>The current instance.</returns>
|
|
public DelayedTask WithExceptionHandler(Action<Exception> exceptionHandler)
|
|
{
|
|
this.exceptionHandler = exceptionHandler;
|
|
return this;
|
|
}
|
|
|
|
#endregion Public instance methods
|
|
|
|
#region Non-public methods
|
|
|
|
/// <summary>
|
|
/// Starts the current instance after creating it.
|
|
/// </summary>
|
|
/// <returns>The current instance.</returns>
|
|
protected DelayedTask Start()
|
|
{
|
|
tcs = CreateTcs();
|
|
IsWaitingToRun = true;
|
|
_timer = new Timer(OnTimerCallback, null, Delay, Timeout.InfiniteTimeSpan);
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a <see cref="TaskCompletionSourceWrapper"/> instance.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected virtual TaskCompletionSourceWrapper CreateTcs()
|
|
=> new TaskCompletionSourceWrapper<object>();
|
|
|
|
/// <summary>
|
|
/// Called when the timer has elapsed.
|
|
/// </summary>
|
|
/// <param name="state">Unused.</param>
|
|
protected void OnTimerCallback(object state)
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
if (!IsWaitingToRun)
|
|
{
|
|
// Already cancelled, do nothing
|
|
return;
|
|
}
|
|
|
|
IsWaitingToRun = false;
|
|
if (IsRunning)
|
|
{
|
|
// Currently running, remember and do nothing for now
|
|
_nextRunPending = true;
|
|
return;
|
|
}
|
|
IsRunning = true;
|
|
}
|
|
|
|
// Run as long as there are pending executions and the instance has not been disposed of
|
|
bool runAgain;
|
|
TaskCompletionSourceWrapper localTcs = null;
|
|
Exception exception = null;
|
|
do
|
|
{
|
|
try
|
|
{
|
|
Run();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
exception = ex;
|
|
lock (syncLock)
|
|
{
|
|
runAgain = false;
|
|
IsRunning = false;
|
|
_nextRunPending = false;
|
|
localTcs = tcs;
|
|
if (!IsWaitingToRun)
|
|
{
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
}
|
|
}
|
|
exceptionHandler?.Invoke(ex);
|
|
}
|
|
finally
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
runAgain = _nextRunPending;
|
|
IsRunning = runAgain;
|
|
_nextRunPending = false;
|
|
if (!runAgain)
|
|
{
|
|
if (!IsWaitingToRun)
|
|
{
|
|
localTcs = tcs;
|
|
_timer?.Dispose();
|
|
_timer = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while (runAgain);
|
|
|
|
// Unblock waiters if not already waiting for the next execution.
|
|
// This task can be awaited again after the Reset method has been called.
|
|
if (exception != null)
|
|
{
|
|
localTcs?.TrySetException(exception);
|
|
}
|
|
else
|
|
{
|
|
SetLastResult(localTcs);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the action of the task.
|
|
/// </summary>
|
|
protected virtual void Run() => Action();
|
|
|
|
/// <summary>
|
|
/// Sets the <see cref="TaskCompletionSourceWrapper"/> result from the last action.
|
|
/// </summary>
|
|
/// <param name="tcs">The <see cref="TaskCompletionSourceWrapper"/> to set the result of.</param>
|
|
protected virtual void SetLastResult(TaskCompletionSourceWrapper tcs)
|
|
{
|
|
var myTcs = (TaskCompletionSourceWrapper<object>)tcs;
|
|
myTcs?.TrySetResult(default);
|
|
}
|
|
|
|
#endregion Non-public methods
|
|
|
|
#region Internal TaskCompletionSourceWrapper classes
|
|
|
|
/// <summary>
|
|
/// Wraps a <see cref="TaskCompletionSource{TResult}"/> instance in a non-generic way to
|
|
/// allow sharing it in the non-generic base class.
|
|
/// </summary>
|
|
protected abstract class TaskCompletionSourceWrapper
|
|
{
|
|
/// <summary>
|
|
/// Gets the <see cref="Task{TResult}"/> of the <see cref="TaskCompletionSource{TResult}"/>.
|
|
/// </summary>
|
|
public abstract Task Task { get; }
|
|
|
|
/// <summary>
|
|
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
|
/// <see cref="TaskStatus.Faulted"/> state and binds it to a specified exception.
|
|
/// </summary>
|
|
/// <param name="exception">The exception to bind to this <see cref="Task{TResult}"/>.</param>
|
|
/// <seealso cref="TaskCompletionSource{TResult}.TrySetException(Exception)"/>
|
|
public abstract void TrySetException(Exception exception);
|
|
|
|
/// <summary>
|
|
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
|
/// <see cref="TaskStatus.Canceled"/> state.
|
|
/// </summary>
|
|
/// <seealso cref="TaskCompletionSource{TResult}.TrySetCanceled()"/>
|
|
public abstract void TrySetCanceled();
|
|
}
|
|
|
|
/// <summary>
|
|
/// A <see cref="TaskCompletionSourceWrapper"/> that provides a result value.
|
|
/// </summary>
|
|
/// <typeparam name="TResult">The type of the result value.</typeparam>
|
|
protected class TaskCompletionSourceWrapper<TResult> : TaskCompletionSourceWrapper
|
|
{
|
|
private readonly TaskCompletionSource<TResult> _tcs;
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="Task{TResult}"/> of the <see cref="TaskCompletionSource{TResult}"/>.
|
|
/// </summary>
|
|
public override Task Task => _tcs.Task;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="TaskCompletionSourceWrapper{TResult}"/> class.
|
|
/// </summary>
|
|
public TaskCompletionSourceWrapper()
|
|
{
|
|
_tcs = new TaskCompletionSource<TResult>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to transition the underlying <see cref="Task{TResult}"/> into the
|
|
/// <see cref="TaskStatus.RanToCompletion"/> state.
|
|
/// </summary>
|
|
/// <param name="result">The result value to bind to this <see cref="Task{TResult}"/>.</param>
|
|
/// <seealso cref="TaskCompletionSource{TResult}.TrySetResult(TResult)"/>
|
|
public void TrySetResult(TResult result) => _tcs.TrySetResult(result);
|
|
|
|
/// <inheritdoc/>
|
|
public override void TrySetException(Exception exception) => _tcs.TrySetException(exception);
|
|
|
|
/// <inheritdoc/>
|
|
public override void TrySetCanceled() => _tcs.TrySetCanceled();
|
|
}
|
|
|
|
#endregion Internal TaskCompletionSourceWrapper classes
|
|
}
|
|
|
|
#region Generic derived classes
|
|
|
|
/// <summary>
|
|
/// Implements an awaitable task that runs after a specified delay. The delay can be reset
|
|
/// before and after the task has run.
|
|
/// </summary>
|
|
/// <typeparam name="TResult">The type of the return value of the action.</typeparam>
|
|
public class DelayedTaskWithResult<TResult> : DelayedTask
|
|
{
|
|
/// <summary>
|
|
/// The result of the last execution of the action.
|
|
/// </summary>
|
|
protected TResult lastResult;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the action to execute.
|
|
/// </summary>
|
|
protected new Func<TResult> Action { get; set; }
|
|
|
|
internal static DelayedTaskWithResult<TResult> Create(Func<TResult> action, TimeSpan delay)
|
|
=> new() { Action = action, Delay = delay };
|
|
|
|
internal static DelayedTaskWithResult<TResult> Run(Func<TResult> action, TimeSpan delay)
|
|
=> (DelayedTaskWithResult<TResult>)new DelayedTaskWithResult<TResult> { Action = action, Delay = delay }.Start();
|
|
|
|
/// <inheritdoc/>
|
|
protected override TaskCompletionSourceWrapper CreateTcs()
|
|
=> new TaskCompletionSourceWrapper<TResult>();
|
|
|
|
/// <inheritdoc/>
|
|
protected override void Run()
|
|
{
|
|
lastResult = Action();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override void SetLastResult(TaskCompletionSourceWrapper tcs)
|
|
{
|
|
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
|
myTcs?.TrySetResult(lastResult);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an awaiter used to await this <see cref="DelayedTask"/>.
|
|
/// </summary>
|
|
/// <returns>An awaiter instance.</returns>
|
|
public new TaskAwaiter<TResult> GetAwaiter()
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
|
var myTask = (Task<TResult>)myTcs.Task;
|
|
return myTask.GetAwaiter();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="Task{TResult}"/> that represents the current awaitable operation.
|
|
/// </summary>
|
|
public new Task<TResult> Task
|
|
{
|
|
get
|
|
{
|
|
lock (syncLock)
|
|
{
|
|
var myTcs = (TaskCompletionSourceWrapper<TResult>)tcs;
|
|
var myTask = (Task<TResult>)myTcs.Task;
|
|
return myTask;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs an implicit conversion from <see cref="DelayedTaskWithResult{TResult}"/> to
|
|
/// <see cref="Task{TResult}"/>.
|
|
/// </summary>
|
|
/// <param name="delayedTask">The <see cref="DelayedTaskWithResult{TResult}"/> instance to cast.</param>
|
|
/// <returns>A <see cref="Task{TResult}"/> that represents the current awaitable operation.</returns>
|
|
public static implicit operator Task<TResult>(DelayedTaskWithResult<TResult> delayedTask)
|
|
=> delayedTask.Task;
|
|
|
|
/// <summary>
|
|
/// Adds an unhandled exception handler to this <see cref="DelayedTask"/> instance.
|
|
/// </summary>
|
|
/// <param name="exceptionHandler">The action that handles an exception.</param>
|
|
/// <returns>The current instance.</returns>
|
|
public new DelayedTaskWithResult<TResult> WithExceptionHandler(Action<Exception> exceptionHandler)
|
|
{
|
|
this.exceptionHandler = exceptionHandler;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
#endregion Generic derived classes
|
|
}
|