/// <summary> /// Initializes a new instance of the <see cref="AsyncTimer" /> class. /// </summary> /// <param name="callback">The asynchronous method to be executed.</param> /// <param name="period">The minimum gap between the start of the task invocation and the start of the previous task invocation (defautls to <see langword="null" /> which is equivalent to <see cref="TimeHelpers.InfiniteDuration" />).</param> /// <param name="dueTime">The due time between the last time the timeouts were changed and the start of the task invocation (defautls to <see langword="null" /> which is equivalent to <see cref="Duration.Zero" />).</param> /// <param name="minimumGap">The minimum gap between the start of the task invocation and the end of the previous task invocation (defautls to <see langword="null" /> which is equivalent to <see cref="Duration.Zero" />).</param> /// <param name="pauseToken">The pause token for pasuing the timer.</param> /// <param name="errorHandler">The optional error handler.</param> public AsyncTimer( [NotNull] AsyncTimerCallback callback, Duration?period = null, Duration?dueTime = null, Duration?minimumGap = null, PauseToken pauseToken = default(PauseToken), Action <Exception> errorHandler = null) { if (callback == null) { throw new ArgumentNullException(nameof(callback)); } long timeStamp = HighPrecisionClock.Instance.NowTicks; _callback = callback; _pauseToken = pauseToken; _timeOuts = new TimeOuts( period ?? TimeHelpers.InfiniteDuration, dueTime ?? Duration.Zero, minimumGap ?? Duration.Zero, timeStamp); _cancellationTokenSource = new CancellationTokenSource(); _timeOutsChanged = new CancellationTokenSource(); _callbackCompletionSource = null; _errorHandler = errorHandler; Task.Run(() => TimerTask(_cancellationTokenSource.Token), _cancellationTokenSource.Token); }
/// <summary> /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// </summary> public void Dispose() { CancellationTokenSource cts = Interlocked.Exchange(ref _cancellationTokenSource, null); if (!ReferenceEquals(cts, null)) { cts.Cancel(); cts.Dispose(); } CancellationTokenSource pdc = Interlocked.Exchange(ref _timeOutsChanged, null); if (!ReferenceEquals(pdc, null)) { pdc.Cancel(); pdc.Dispose(); } TaskCompletionSource <bool> tcs = Interlocked.Exchange(ref _callbackCompletionSource, null); if (!ReferenceEquals(tcs, null)) { tcs.TrySetCanceled(); } _timeOuts = null; }
/// <summary> /// Changes the specified due time and period. /// </summary> /// <param name="period">The optional minimum gap between the start of the task invocation and the start of the previous task invocation; use <see langword="null"/> to leave the value unchanged.</param> /// <param name="dueTime">The optional due time between the last time the timeouts were changed and the start of the task invocation; use <see langword="null"/> to leave the value unchanged.</param> /// <param name="minimumGap">The optional minimum gap between the start of the task invocation and the end of the previous task invocation; use <see langword="null"/> to leave the value unchanged.</param> public void Change(Duration?period = null, Duration?dueTime = null, Duration?minimumGap = null) { long timeStamp = HighPrecisionClock.Instance.NowTicks; bool dueTimeChanged = !ReferenceEquals(dueTime, null); bool minimumGapChanged = !ReferenceEquals(minimumGap, null); bool periodChanged = !ReferenceEquals(period, null); if (dueTimeChanged && minimumGapChanged && periodChanged) { // Changing everything so we can just go ahead and change CancellationTokenSource timeOutsChanged = _timeOutsChanged; // If we don't have a cancellation token we're disposed if (ReferenceEquals(timeOutsChanged, null)) { throw new ObjectDisposedException("AsyncTimer"); } // Update the timeOuts and cancel timeOutsChanged, as timeOuts includes a timestamp it always changes. _timeOuts = new TimeOuts(period.Value, dueTime.Value, minimumGap.Value, timeStamp); timeOutsChanged.Cancel(); } else if (dueTimeChanged || minimumGapChanged || periodChanged) { TimeOuts newTimeOuts, oldTimeOuts; // We're changing at least one thing do { oldTimeOuts = _timeOuts; // Check we have timeouts (might be disposed) if (ReferenceEquals(oldTimeOuts, null)) { return; } // If the current timestamp is newer than this ignore if (oldTimeOuts.TimeStamp >= timeStamp) { return; } newTimeOuts = new TimeOuts( period ?? oldTimeOuts.Period, dueTime ?? oldTimeOuts.DueTime, minimumGap ?? oldTimeOuts.MinimumGap, timeStamp, dueTime.HasValue ? (long?)null : oldTimeOuts.DueTimeStamp); } while (Interlocked.CompareExchange(ref _timeOuts, newTimeOuts, oldTimeOuts) != oldTimeOuts); CancellationTokenSource timeOutsChanged = _timeOutsChanged; // If we don't have a cancellation token we're disposed if (ReferenceEquals(timeOutsChanged, null)) { throw new ObjectDisposedException("AsyncTimer"); } timeOutsChanged.Cancel(); } }
/// <summary> /// The timer task executes the callback asynchronously after set delays. /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns></returns> // ReSharper disable once FunctionComplexityOverflow private async Task TimerTask(CancellationToken cancellationToken) { long startTicks = long.MinValue; long endTicks = long.MinValue; while (!cancellationToken.IsCancellationRequested) { try { CancellationTokenSource timeoutsChanged; // Check we're not set to run immediately if (Interlocked.Exchange(ref _runImmediate, 0) == 0) { do { // Create new cancellation token source and set _timeOutsChanged to it in a thread-safe none-locking way. timeoutsChanged = new CancellationTokenSource(); CancellationTokenSource toc = Interlocked.Exchange(ref _timeOutsChanged, timeoutsChanged); if (ReferenceEquals(toc, null)) { toc = Interlocked.CompareExchange(ref _timeOutsChanged, null, timeoutsChanged); if (!ReferenceEquals(toc, null)) { toc.Dispose(); } return; } // If we have run immediate set at this point, we can't rely on the correct _timeOutsChanged cts being cancelled. if (Interlocked.Exchange(ref _runImmediate, 0) > 0) { break; } using (ITokenSource tokenSource = cancellationToken.CreateLinked(timeoutsChanged.Token)) { // Check for pausing. try { await _pauseToken.WaitWhilePausedAsync(tokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { } catch (Exception exception) { if (!ReferenceEquals(_errorHandler, null)) { _errorHandler(exception); } } if (cancellationToken.IsCancellationRequested) { return; } // Get timeouts TimeOuts timeOuts = _timeOuts; if (ReferenceEquals(timeOuts, null)) { return; } if (timeOuts.DueTimeMs < 0 || (startTicks > timeOuts.DueTimeStamp && (timeOuts.MinimumGapMs < 0 || timeOuts.PeriodMs < 0))) { // If we have infinite waits then we are effectively awaiting cancellation // ReSharper disable once PossibleNullReferenceException await tokenSource.ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) { return; } continue; } // If all timeouts are zero we effectively run again immediately (after checking we didn't get a cancellation // indicating the value have changed again). if (timeOuts.DueTimeMs == 0 && timeOuts.MinimumGapMs == 0 && timeOuts.PeriodMs == 0) { continue; } int wait; if (startTicks > long.MinValue) { // Calculate the wait time based on the minimum gap and the period. long now = HighPrecisionClock.Instance.NowTicks; int a = timeOuts.PeriodMs - (int)((now - startTicks) / NodaConstants.TicksPerMillisecond); int b = timeOuts.MinimumGapMs - (int)((now - endTicks) / NodaConstants.TicksPerMillisecond); int c = (int)((timeOuts.DueTimeStamp - now) / NodaConstants.TicksPerMillisecond); wait = Math.Max(a, Math.Max(b, c)); } else { // Wait the initial due time wait = (int) ((timeOuts.DueTimeStamp - HighPrecisionClock.Instance.NowTicks) / NodaConstants.TicksPerMillisecond); } // If we don't need to wait run again immediately (after checking values haven't changed). if (wait < 1) { continue; } try { // Wait for set milliseconds // ReSharper disable PossibleNullReferenceException await Task.Delay(wait, tokenSource.Token).ConfigureAwait(false); // ReSharper restore PossibleNullReferenceException } catch (OperationCanceledException) { } catch (Exception exception) { if (!ReferenceEquals(_errorHandler, null)) { _errorHandler(exception); } } } // Recalculate wait time if 'cancelled' due to signal, and not set to run immediately; or if we're currently paused. } while ( _pauseToken.IsPaused || (timeoutsChanged.IsCancellationRequested && !cancellationToken.IsCancellationRequested && Interlocked.Exchange(ref _runImmediate, 0) < 1)); } if (cancellationToken.IsCancellationRequested) { return; } try { Interlocked.CompareExchange( ref _callbackCompletionSource, new TaskCompletionSource <bool>(), null); startTicks = HighPrecisionClock.Instance.NowTicks; // ReSharper disable once PossibleNullReferenceException await _callback(cancellationToken).ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) { return; } } catch (OperationCanceledException) { // Just finish as we're cancelled TaskCompletionSource <bool> callbackCompletionSource = Interlocked.Exchange(ref _callbackCompletionSource, null); // If the completion source is not null, then someone is awaiting last execution, so complete the task if (!ReferenceEquals(callbackCompletionSource, null)) { callbackCompletionSource.TrySetCanceled(); } return; } // ReSharper disable once EmptyGeneralCatchClause catch (Exception exception) { // Supress errors thrown by callback, unless someone is awaiting it. TaskCompletionSource <bool> callbackCompletionSource = Interlocked.Exchange(ref _callbackCompletionSource, null); // If the completion source is not null, then someone is awaiting last execution, so complete the task if (!ReferenceEquals(callbackCompletionSource, null)) { callbackCompletionSource.TrySetException(exception); } if (!ReferenceEquals(_errorHandler, null)) { _errorHandler(exception); } } finally { endTicks = HighPrecisionClock.Instance.NowTicks; // If run immediately was set whilst we were running, we can clear it. Interlocked.Exchange(ref _runImmediate, 0); TaskCompletionSource <bool> callbackCompletionSource = Interlocked.Exchange(ref _callbackCompletionSource, null); // If the completion source is not null, then someone is awaiting last execution, so complete the task if (!ReferenceEquals(callbackCompletionSource, null)) { callbackCompletionSource.TrySetResult(true); } } } catch (Exception exception) { if (!ReferenceEquals(_errorHandler, null)) { _errorHandler(exception); } } } }
/// <summary> /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// </summary> public void Dispose() { CancellationTokenSource cts = Interlocked.Exchange(ref _cancellationTokenSource, null); if (!ReferenceEquals(cts, null)) { cts.Cancel(); cts.Dispose(); } CancellationTokenSource pdc = Interlocked.Exchange(ref _timeOutsChanged, null); if (!ReferenceEquals(pdc, null)) { pdc.Cancel(); pdc.Dispose(); } TaskCompletionSource<bool> tcs = Interlocked.Exchange(ref _callbackCompletionSource, null); if (!ReferenceEquals(tcs, null)) tcs.TrySetCanceled(); _timeOuts = null; }
/// <summary> /// Changes the specified due time and period. /// </summary> /// <param name="period">The optional minimum gap between the start of the task invocation and the start of the previous task invocation; use <see langword="null"/> to leave the value unchanged.</param> /// <param name="dueTime">The optional due time between the last time the timeouts were changed and the start of the task invocation; use <see langword="null"/> to leave the value unchanged.</param> /// <param name="minimumGap">The optional minimum gap between the start of the task invocation and the end of the previous task invocation; use <see langword="null"/> to leave the value unchanged.</param> public void Change(Duration? period = null, Duration? dueTime = null, Duration? minimumGap = null) { long timeStamp = HighPrecisionClock.Instance.NowTicks; bool dueTimeChanged = !ReferenceEquals(dueTime, null); bool minimumGapChanged = !ReferenceEquals(minimumGap, null); bool periodChanged = !ReferenceEquals(period, null); if (dueTimeChanged && minimumGapChanged && periodChanged) { // Changing everything so we can just go ahead and change CancellationTokenSource timeOutsChanged = _timeOutsChanged; // If we don't have a cancellation token we're disposed if (ReferenceEquals(timeOutsChanged, null)) throw new ObjectDisposedException("AsyncTimer"); // Update the timeOuts and cancel timeOutsChanged, as timeOuts includes a timestamp it always changes. _timeOuts = new TimeOuts(period.Value, dueTime.Value, minimumGap.Value, timeStamp); timeOutsChanged.Cancel(); } else if (dueTimeChanged || minimumGapChanged || periodChanged) { TimeOuts newTimeOuts, oldTimeOuts; // We're changing at least one thing do { oldTimeOuts = _timeOuts; // Check we have timeouts (might be disposed) if (ReferenceEquals(oldTimeOuts, null)) return; // If the current timestamp is newer than this ignore if (oldTimeOuts.TimeStamp >= timeStamp) return; newTimeOuts = new TimeOuts( period ?? oldTimeOuts.Period, dueTime ?? oldTimeOuts.DueTime, minimumGap ?? oldTimeOuts.MinimumGap, timeStamp, dueTime.HasValue ? (long?)null : oldTimeOuts.DueTimeStamp); } while (Interlocked.CompareExchange(ref _timeOuts, newTimeOuts, oldTimeOuts) != oldTimeOuts); CancellationTokenSource timeOutsChanged = _timeOutsChanged; // If we don't have a cancellation token we're disposed if (ReferenceEquals(timeOutsChanged, null)) throw new ObjectDisposedException("AsyncTimer"); timeOutsChanged.Cancel(); } }
/// <summary> /// Initializes a new instance of the <see cref="AsyncTimer" /> class. /// </summary> /// <param name="callback">The asynchronous method to be executed.</param> /// <param name="period">The minimum gap between the start of the task invocation and the start of the previous task invocation (defautls to <see langword="null" /> which is equivalent to <see cref="TimeHelpers.InfiniteDuration" />).</param> /// <param name="dueTime">The due time between the last time the timeouts were changed and the start of the task invocation (defautls to <see langword="null" /> which is equivalent to <see cref="Duration.Zero" />).</param> /// <param name="minimumGap">The minimum gap between the start of the task invocation and the end of the previous task invocation (defautls to <see langword="null" /> which is equivalent to <see cref="Duration.Zero" />).</param> /// <param name="pauseToken">The pause token for pasuing the timer.</param> /// <param name="errorHandler">The optional error handler.</param> public AsyncTimer( [NotNull] AsyncTimerCallback callback, Duration? period = null, Duration? dueTime = null, Duration? minimumGap = null, PauseToken pauseToken = default(PauseToken), Action<Exception> errorHandler = null) { if (callback == null) throw new ArgumentNullException(nameof(callback)); long timeStamp = HighPrecisionClock.Instance.NowTicks; _callback = callback; _pauseToken = pauseToken; _timeOuts = new TimeOuts( period ?? TimeHelpers.InfiniteDuration, dueTime ?? Duration.Zero, minimumGap ?? Duration.Zero, timeStamp); _cancellationTokenSource = new CancellationTokenSource(); _timeOutsChanged = new CancellationTokenSource(); _callbackCompletionSource = null; _errorHandler = errorHandler; Task.Run(() => TimerTask(_cancellationTokenSource.Token), _cancellationTokenSource.Token); }