// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics;
namespace System.Threading
{
/// Signals to a that it should be canceled.
///
///
/// is used to instantiate a (via
/// the source's Token property) that can be handed to operations that wish to be
/// notified of cancellation or that can be used to register asynchronous operations for cancellation. That
/// token may have cancellation requested by calling to the source's method.
///
///
/// All members of this class, except , are thread-safe and may be used
/// concurrently from multiple threads.
///
///
public class CancellationTokenSource : IDisposable
{
/// A that's already canceled.
internal static readonly CancellationTokenSource s_canceledSource = new CancellationTokenSource() { _state = NotifyingCompleteState };
/// A that's never canceled. This isn't enforced programmatically, only by usage. Do not cancel!
internal static readonly CancellationTokenSource s_neverCanceledSource = new CancellationTokenSource();
/// Delegate used with to trigger cancellation of a .
private static readonly TimerCallback s_timerCallback = obj =>
((CancellationTokenSource)obj).NotifyCancellation(throwOnFirstException: false); // skip ThrowIfDisposed() check in Cancel()
/// The number of callback partitions to use in a . Must be a power of 2.
private static readonly int s_numPartitions = GetPartitionCount();
/// - 1, used to quickly mod into .
private static readonly int s_numPartitionsMask = s_numPartitions - 1;
/// The current state of the CancellationTokenSource.
private volatile int _state;
/// The ID of the thread currently executing the main body of CTS.Cancel()
///
/// This helps us to know if a call to ctr.Dispose() is running 'within' a cancellation callback.
/// This is updated as we move between the main thread calling cts.Cancel() and any syncContexts
/// that are used to actually run the callbacks.
///
private volatile int _threadIDExecutingCallbacks = -1;
/// Tracks the running callback to assist ctr.Dispose() to wait for the target callback to complete.
private long _executingCallbackId;
/// Partitions of callbacks. Split into multiple partitions to help with scalability of registering/unregistering; each is protected by its own lock.
private volatile CallbackPartition[] _callbackPartitions;
/// Timer used by CancelAfter and Timer-related ctors.
private volatile Timer _timer;
/// lazily initialized and returned from .
private volatile ManualResetEvent _kernelEvent;
/// Whether this has been disposed.
private bool _disposed;
// legal values for _state
private const int NotCanceledState = 1;
private const int NotifyingState = 2;
private const int NotifyingCompleteState = 3;
/// Gets whether cancellation has been requested for this .
/// Whether cancellation has been requested for this .
///
///
/// This property indicates whether cancellation has been requested for this token source, such as
/// due to a call to its method.
///
///
/// If this property returns true, it only guarantees that cancellation has been requested. It does not
/// guarantee that every handler registered with the corresponding token has finished executing, nor
/// that cancellation requests have finished propagating to all registered handlers. Additional
/// synchronization may be required, particularly in situations where related objects are being
/// canceled concurrently.
///
///
public bool IsCancellationRequested => _state >= NotifyingState;
/// A simple helper to determine whether cancellation has finished.
internal bool IsCancellationCompleted => _state == NotifyingCompleteState;
/// A simple helper to determine whether disposal has occurred.
internal bool IsDisposed => _disposed;
/// The ID of the thread that is running callbacks.
internal int ThreadIDExecutingCallbacks
{
get => _threadIDExecutingCallbacks;
set => _threadIDExecutingCallbacks = value;
}
/// Gets the associated with this .
/// The associated with this .
/// The token source has been disposed.
public CancellationToken Token
{
get
{
ThrowIfDisposed();
return new CancellationToken(this);
}
}
internal WaitHandle WaitHandle
{
get
{
ThrowIfDisposed();
// Return the handle if it was already allocated.
if (_kernelEvent != null)
{
return _kernelEvent;
}
// Lazily-initialize the handle.
var mre = new ManualResetEvent(false);
if (Interlocked.CompareExchange(ref _kernelEvent, mre, null) != null)
{
mre.Dispose();
}
// There is a race condition between checking IsCancellationRequested and setting the event.
// However, at this point, the kernel object definitely exists and the cases are:
// 1. if IsCancellationRequested = true, then we will call Set()
// 2. if IsCancellationRequested = false, then NotifyCancellation will see that the event exists, and will call Set().
if (IsCancellationRequested)
{
_kernelEvent.Set();
}
return _kernelEvent;
}
}
/// Gets the ID of the currently executing callback.
internal long ExecutingCallback => Volatile.Read(ref _executingCallbackId);
/// Initializes the .
public CancellationTokenSource() => _state = NotCanceledState;
///
/// Constructs a that will be canceled after a specified time span.
///
/// The time span to wait before canceling this
///
/// The exception that is thrown when is less than -1 or greater than Int32.MaxValue.
///
///
///
/// The countdown for the delay starts during the call to the constructor. When the delay expires,
/// the constructed is canceled, if it has
/// not been canceled already.
///
///
/// Subsequent calls to CancelAfter will reset the delay for the constructed
/// , if it has not been
/// canceled already.
///
///
public CancellationTokenSource(TimeSpan delay)
{
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
{
throw new ArgumentOutOfRangeException(nameof(delay));
}
InitializeWithTimer((int)totalMilliseconds);
}
///
/// Constructs a that will be canceled after a specified time span.
///
/// The time span to wait before canceling this
///
/// The exception that is thrown when is less than -1.
///
///
///
/// The countdown for the millisecondsDelay starts during the call to the constructor. When the millisecondsDelay expires,
/// the constructed is canceled (if it has
/// not been canceled already).
///
///
/// Subsequent calls to CancelAfter will reset the millisecondsDelay for the constructed
/// , if it has not been
/// canceled already.
///
///
public CancellationTokenSource(int millisecondsDelay)
{
if (millisecondsDelay < -1)
{
throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
}
InitializeWithTimer(millisecondsDelay);
}
/// Common initialization logic when constructing a CTS with a delay parameter
private void InitializeWithTimer(int millisecondsDelay)
{
_state = NotCanceledState;
_timer = new Timer(s_timerCallback, this, millisecondsDelay, -1);
}
/// Communicates a request for cancellation.
///
///
/// The associated will be notified of the cancellation
/// and will transition to a state where returns true.
/// Any callbacks or cancelable operations registered with the will be executed.
///
///
/// Cancelable operations and callbacks registered with the token should not throw exceptions.
/// However, this overload of Cancel will aggregate any exceptions thrown into a ,
/// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
///
///
/// The that was captured when each callback was registered
/// will be reestablished when the callback is invoked.
///
///
/// An aggregate exception containing all the exceptions thrown
/// by the registered callbacks on the associated .
/// This has been disposed.
public void Cancel() => Cancel(false);
/// Communicates a request for cancellation.
///
///
/// The associated will be notified of the cancellation and will transition to a state where
/// returns true. Any callbacks or cancelable operationsregistered
/// with the will be executed.
///
///
/// Cancelable operations and callbacks registered with the token should not throw exceptions.
/// If is true, an exception will immediately propagate out of the
/// call to Cancel, preventing the remaining callbacks and cancelable operations from being processed.
/// If is false, this overload will aggregate any
/// exceptions thrown into a ,
/// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
///
///
/// The that was captured when each callback was registered
/// will be reestablished when the callback is invoked.
///
///
/// Specifies whether exceptions should immediately propagate.
/// An aggregate exception containing all the exceptions thrown
/// by the registered callbacks on the associated .
/// This has been disposed.
public void Cancel(bool throwOnFirstException)
{
ThrowIfDisposed();
NotifyCancellation(throwOnFirstException);
}
/// Schedules a Cancel operation on this .
/// The time span to wait before canceling this .
///
/// The exception thrown when this has been disposed.
///
///
/// The exception thrown when is less than -1 or
/// greater than Int32.MaxValue.
///
///
///
/// The countdown for the delay starts during this call. When the delay expires,
/// this is canceled, if it has
/// not been canceled already.
///
///
/// Subsequent calls to CancelAfter will reset the delay for this
/// , if it has not been canceled already.
///
///
public void CancelAfter(TimeSpan delay)
{
long totalMilliseconds = (long)delay.TotalMilliseconds;
if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
{
throw new ArgumentOutOfRangeException(nameof(delay));
}
CancelAfter((int)totalMilliseconds);
}
///
/// Schedules a Cancel operation on this .
///
/// The time span to wait before canceling this .
///
/// The exception thrown when this has been disposed.
///
///
/// The exception thrown when is less than -1.
///
///
///
/// The countdown for the millisecondsDelay starts during this call. When the millisecondsDelay expires,
/// this is canceled, if it has
/// not been canceled already.
///
///
/// Subsequent calls to CancelAfter will reset the millisecondsDelay for this
/// , if it has not been
/// canceled already.
///
///
public void CancelAfter(int millisecondsDelay)
{
ThrowIfDisposed();
if (millisecondsDelay < -1)
{
throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
}
if (IsCancellationRequested)
{
return;
}
// There is a race condition here as a Cancel could occur between the check of
// IsCancellationRequested and the creation of the timer. This is benign; in the
// worst case, a timer will be created that has no effect when it expires.
// Also, if Dispose() is called right here (after ThrowIfDisposed(), before timer
// creation), it would result in a leaked Timer object (at least until the timer
// expired and Disposed itself). But this would be considered bad behavior, as
// Dispose() is not thread-safe and should not be called concurrently with CancelAfter().
if (_timer == null)
{
// Lazily initialize the timer in a thread-safe fashion.
// Initially set to "never go off" because we don't want to take a
// chance on a timer "losing" the initialization and then
// cancelling the token before it (the timer) can be disposed.
Timer newTimer = new Timer(s_timerCallback, this, -1, -1);
if (Interlocked.CompareExchange(ref _timer, newTimer, null) != null)
{
// We did not initialize the timer. Dispose the new timer.
newTimer.Dispose();
}
}
// It is possible that m_timer has already been disposed, so we must do
// the following in a try/catch block.
try
{
_timer.Change(millisecondsDelay, -1);
}
catch (ObjectDisposedException)
{
// Just eat the exception. There is no other way to tell that
// the timer has been disposed, and even if there were, there
// would not be a good way to deal with the observe/dispose
// race condition.
}
}
/// Releases the resources used by this .
/// This method is not thread-safe for any other concurrent calls.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the class and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (disposing && !_disposed)
{
// We specifically tolerate that a callback can be deregistered
// after the CTS has been disposed and/or concurrently with cts.Dispose().
// This is safe without locks because Dispose doesn't interact with values
// in the callback partitions, only nulling out the ref to existing partitions.
//
// We also tolerate that a callback can be registered after the CTS has been
// disposed. This is safe because InternalRegister is tolerant
// of _callbackPartitions becoming null during its execution. However,
// we run the acceptable risk of _callbackPartitions getting reinitialized
// to non-null if there is a race between Dispose and Register, in which case this
// instance may unnecessarily hold onto a registered callback. But that's no worse
// than if Dispose wasn't safe to use concurrently, as Dispose would never be called,
// and thus no handlers would be dropped.
//
// And, we tolerate Dispose being used concurrently with Cancel. This is necessary
// to properly support, e.g., LinkedCancellationTokenSource, where, due to common usage patterns,
// it's possible for this pairing to occur with valid usage (e.g. a component accepts
// an external CancellationToken and uses CreateLinkedTokenSource to combine it with an
// internal source of cancellation, then Disposes of that linked source, which could
// happen at the same time the external entity is requesting cancellation).
_timer?.Dispose(); // Timer.Dispose is thread-safe
_callbackPartitions = null; // free for GC; Cancel correctly handles a null field
// If a kernel event was created via WaitHandle, we'd like to Dispose of it. However,
// we only want to do so if it's not being used by Cancel concurrently. First, we
// interlocked exchange it to be null, and then we check whether cancellation is currently
// in progress. NotifyCancellation will only try to set the event if it exists after it's
// transitioned to and while it's in the NotifyingState.
if (_kernelEvent != null)
{
ManualResetEvent mre = Interlocked.Exchange(ref _kernelEvent, null);
if (mre != null && _state != NotifyingState)
{
mre.Dispose();
}
}
_disposed = true;
}
}
/// Throws an exception if the source has been disposed.
private void ThrowIfDisposed()
{
if (_disposed)
{
ThrowObjectDisposedException();
}
}
/// Throws an . Separated out from ThrowIfDisposed to help with inlining.
private static void ThrowObjectDisposedException() =>
throw new ObjectDisposedException(null, SR.CancellationTokenSource_Disposed);
///
/// Registers a callback object. If cancellation has already occurred, the
/// callback will have been run by the time this method returns.
///
internal CancellationTokenRegistration InternalRegister(
Action