// 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.
#pragma warning disable 0420
//
////////////////////////////////////////////////////////////////////////////////
using System;
using System.Security;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime;
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 Cancel
/// method.
///
///
/// All members of this class, except Dispose, are thread-safe and may be used
/// concurrently from multiple threads.
///
///
public class CancellationTokenSource : IDisposable
{
//static sources that can be used as the backing source for 'fixed' CancellationTokens that never change state.
private static readonly CancellationTokenSource _staticSource_Set = new CancellationTokenSource(true);
private static readonly CancellationTokenSource _staticSource_NotCancelable = new CancellationTokenSource(false);
//Note: the callback lists array is only created on first registration.
// the actual callback lists are only created on demand.
// Storing a registered callback costs around >60bytes, hence some overhead for the lists array is OK
// At most 24 lists seems reasonable, and caps the cost of the listsArray to 96bytes(32-bit,24-way) or 192bytes(64-bit,24-way).
private static readonly int s_nLists = (PlatformHelper.ProcessorCount > 24) ? 24 : PlatformHelper.ProcessorCount;
private volatile ManualResetEvent m_kernelEvent; //lazily initialized if required.
private volatile SparselyPopulatedArray[] m_registeredCallbacksLists;
// legal values for m_state
private const int CANNOT_BE_CANCELED = 0;
private const int NOT_CANCELED = 1;
private const int NOTIFYING = 2;
private const int NOTIFYINGCOMPLETE = 3;
//m_state uses the pattern "volatile int32 reads, with cmpxch writes" which is safe for updates and cannot suffer torn reads.
private volatile int m_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 m_threadIDExecutingCallbacks = -1;
private bool m_disposed;
// we track the running callback to assist ctr.Dispose() to wait for the target callback to complete.
private volatile CancellationCallbackInfo m_executingCallback;
// provided for CancelAfter and timer-related constructors
private volatile Timer m_timer;
// ----------------------
// ** public properties
///
/// Gets whether cancellation has been requested for this CancellationTokenSource.
///
/// Whether cancellation has been requested for this CancellationTokenSource.
///
///
/// This property indicates whether cancellation has been requested for this token source, such as
/// due to a call to its
/// Cancel 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
{
get { return m_state >= NOTIFYING; }
}
///
/// A simple helper to determine whether cancellation has finished.
///
internal bool IsCancellationCompleted
{
get { return m_state == NOTIFYINGCOMPLETE; }
}
///
/// A simple helper to determine whether disposal has occurred.
///
internal bool IsDisposed
{
get { return m_disposed; }
}
///
/// The ID of the thread that is running callbacks.
///
internal int ThreadIDExecutingCallbacks
{
set { m_threadIDExecutingCallbacks = value; }
get { return m_threadIDExecutingCallbacks; }
}
///
/// Gets the CancellationToken
/// associated with this .
///
/// The CancellationToken
/// associated with this .
/// The token source has been
/// disposed.
public CancellationToken Token
{
get
{
ThrowIfDisposed();
return new CancellationToken(this);
}
}
// ----------------------
// ** internal and private properties.
///
///
///
internal bool CanBeCanceled
{
get { return m_state != CANNOT_BE_CANCELED; }
}
///
///
///
internal WaitHandle WaitHandle
{
get
{
ThrowIfDisposed();
// fast path if already allocated.
if (m_kernelEvent != null)
return m_kernelEvent;
// lazy-init the mre.
ManualResetEvent mre = new ManualResetEvent(false);
if (Interlocked.CompareExchange(ref m_kernelEvent, mre, null) != null)
{
((IDisposable)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)
m_kernelEvent.Set();
return m_kernelEvent;
}
}
///
/// The currently executing callback
///
internal CancellationCallbackInfo ExecutingCallback
{
get { return m_executingCallback; }
}
#if DEBUG
///
/// Used by the dev unit tests to check the number of outstanding registrations.
/// They use private reflection to gain access. Because this would be dead retail
/// code, however, it is ifdef'd out to work only in debug builds.
///
private int CallbackCount
{
get
{
SparselyPopulatedArray[] callbackLists = m_registeredCallbacksLists;
if (callbackLists == null)
return 0;
int count = 0;
foreach(SparselyPopulatedArray sparseArray in callbackLists)
{
if(sparseArray != null)
{
SparselyPopulatedArrayFragment currCallbacks = sparseArray.Head;
while (currCallbacks != null)
{
for (int i = 0; i < currCallbacks.Length; i++)
if (currCallbacks[i] != null)
count++;
currCallbacks = currCallbacks.Next;
}
}
}
return count;
}
}
#endif
// ** Public Constructors
///
/// Initializes the .
///
public CancellationTokenSource()
{
m_state = NOT_CANCELED;
}
// ** Private constructors for static sources.
// set=false ==> cannot be canceled.
// set=true ==> is canceled.
private CancellationTokenSource(bool set)
{
m_state = set ? NOTIFYINGCOMPLETE : CANNOT_BE_CANCELED;
}
///
/// 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 > Int32.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(Int32 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(Int32 millisecondsDelay)
{
m_state = NOT_CANCELED;
m_timer = new Timer(s_timerCallback, this, millisecondsDelay, -1);
}
// ** Public Methods
///
/// Communicates a request for cancellation.
///
///
///
/// The associated will be
/// notified of the cancellation and will transition to a state where
/// IsCancellationRequested 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
/// IsCancellationRequested 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.
/// 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 > Int32.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(Int32 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 (m_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 m_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
{
m_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.
}
}
private static readonly TimerCallback s_timerCallback = new TimerCallback(TimerCallbackLogic);
// Common logic for a timer delegate
private static void TimerCallbackLogic(object obj)
{
CancellationTokenSource cts = (CancellationTokenSource)obj;
// Cancel the source; handle a race condition with cts.Dispose()
if (!cts.IsDisposed)
{
// There is a small window for a race condition where a cts.Dispose can sneak
// in right here. I'll wrap the cts.Cancel() in a try/catch to proof us
// against this race condition.
try
{
cts.Cancel(); // will take care of disposing of m_timer
}
catch (ObjectDisposedException)
{
// If the ODE was not due to the target cts being disposed, then propagate the ODE.
if (!cts.IsDisposed) throw;
}
}
}
///
/// 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)
{
// There is nothing to do if disposing=false because the CancellationTokenSource holds no unmanaged resources.
if (disposing && !m_disposed)
{
//NOTE: 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 the reg.Dispose() only
// mutates a sparseArrayFragment and then reads from properties of the CTS that are not
// invalidated by cts.Dispose().
//
// We also tolerate that a callback can be registered after the CTS has been
// disposed. This is safe without locks because InternalRegister is tolerant
// of m_registeredCallbacksLists becoming null during its execution. However,
// we run the acceptable risk of m_registeredCallbacksLists 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 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).
m_timer?.Dispose(); // Timer.Dispose is thread-safe
// registered callbacks are now either complete or will never run, due to guarantees made by ctr.Dispose()
// so we can now perform main disposal work without risk of linking callbacks trying to use this CTS.
m_registeredCallbacksLists = 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 NOTIFYING state.
if (m_kernelEvent != null)
{
ManualResetEvent mre = Interlocked.Exchange(ref m_kernelEvent, null);
if (mre != null && m_state != NOTIFYING)
{
mre.Dispose();
}
}
m_disposed = true;
}
}
// -- Internal methods.
///
/// Throws an exception if the source has been disposed.
///
internal void ThrowIfDisposed()
{
if (m_disposed)
ThrowObjectDisposedException();
}
// separation enables inlining of ThrowIfDisposed
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(null, Environment.GetResourceString("CancellationTokenSource_Disposed"));
}
///
/// InternalGetStaticSource()
///
/// Whether the source should be set.
/// A static source to be shared among multiple tokens.
internal static CancellationTokenSource InternalGetStaticSource(bool set)
{
return set ? _staticSource_Set : _staticSource_NotCancelable;
}
///
/// 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