1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Collections.Generic;
6 using System.Diagnostics;
8 namespace System.Threading
10 /// <summary>Signals to a <see cref="CancellationToken"/> that it should be canceled.</summary>
13 /// <see cref="CancellationTokenSource"/> is used to instantiate a <see cref="CancellationToken"/> (via
14 /// the source's <see cref="Token">Token</see> property) that can be handed to operations that wish to be
15 /// notified of cancellation or that can be used to register asynchronous operations for cancellation. That
16 /// token may have cancellation requested by calling to the source's <see cref="Cancel()"/> method.
19 /// All members of this class, except <see cref="Dispose"/>, are thread-safe and may be used
20 /// concurrently from multiple threads.
23 public class CancellationTokenSource : IDisposable
25 /// <summary>A <see cref="CancellationTokenSource"/> that's already canceled.</summary>
26 internal static readonly CancellationTokenSource s_canceledSource = new CancellationTokenSource() { _state = NotifyingCompleteState };
27 /// <summary>A <see cref="CancellationTokenSource"/> that's never canceled. This isn't enforced programmatically, only by usage. Do not cancel!</summary>
28 internal static readonly CancellationTokenSource s_neverCanceledSource = new CancellationTokenSource();
30 /// <summary>Delegate used with <see cref="Timer"/> to trigger cancellation of a <see cref="CancellationTokenSource"/>.</summary>
31 private static readonly TimerCallback s_timerCallback = obj =>
32 ((CancellationTokenSource)obj).NotifyCancellation(throwOnFirstException: false); // skip ThrowIfDisposed() check in Cancel()
34 /// <summary>The number of callback partitions to use in a <see cref="CancellationTokenSource"/>. Must be a power of 2.</summary>
35 private static readonly int s_numPartitions = GetPartitionCount();
36 /// <summary><see cref="s_numPartitions"/> - 1, used to quickly mod into <see cref="_callbackPartitions"/>.</summary>
37 private static readonly int s_numPartitionsMask = s_numPartitions - 1;
39 /// <summary>The current state of the CancellationTokenSource.</summary>
40 private volatile int _state;
41 /// <summary>The ID of the thread currently executing the main body of CTS.Cancel()</summary>
43 /// This helps us to know if a call to ctr.Dispose() is running 'within' a cancellation callback.
44 /// This is updated as we move between the main thread calling cts.Cancel() and any syncContexts
45 /// that are used to actually run the callbacks.
47 private volatile int _threadIDExecutingCallbacks = -1;
48 /// <summary>Tracks the running callback to assist ctr.Dispose() to wait for the target callback to complete.</summary>
49 private long _executingCallbackId;
50 /// <summary>Partitions of callbacks. Split into multiple partitions to help with scalability of registering/unregistering; each is protected by its own lock.</summary>
51 private volatile CallbackPartition[] _callbackPartitions;
52 /// <summary>Timer used by CancelAfter and Timer-related ctors.</summary>
53 private volatile Timer _timer;
54 /// <summary><see cref="System.Threading.WaitHandle"/> lazily initialized and returned from <see cref="WaitHandle"/>.</summary>
55 private volatile ManualResetEvent _kernelEvent;
56 /// <summary>Whether this <see cref="CancellationTokenSource"/> has been disposed.</summary>
57 private bool _disposed;
59 // legal values for _state
60 private const int NotCanceledState = 1;
61 private const int NotifyingState = 2;
62 private const int NotifyingCompleteState = 3;
64 /// <summary>Gets whether cancellation has been requested for this <see cref="CancellationTokenSource" />.</summary>
65 /// <value>Whether cancellation has been requested for this <see cref="CancellationTokenSource" />.</value>
68 /// This property indicates whether cancellation has been requested for this token source, such as
69 /// due to a call to its <see cref="Cancel()"/> method.
72 /// If this property returns true, it only guarantees that cancellation has been requested. It does not
73 /// guarantee that every handler registered with the corresponding token has finished executing, nor
74 /// that cancellation requests have finished propagating to all registered handlers. Additional
75 /// synchronization may be required, particularly in situations where related objects are being
76 /// canceled concurrently.
79 public bool IsCancellationRequested => _state >= NotifyingState;
81 /// <summary>A simple helper to determine whether cancellation has finished.</summary>
82 internal bool IsCancellationCompleted => _state == NotifyingCompleteState;
84 /// <summary>A simple helper to determine whether disposal has occurred.</summary>
85 internal bool IsDisposed => _disposed;
87 /// <summary>The ID of the thread that is running callbacks.</summary>
88 internal int ThreadIDExecutingCallbacks
90 get => _threadIDExecutingCallbacks;
91 set => _threadIDExecutingCallbacks = value;
94 /// <summary>Gets the <see cref="CancellationToken"/> associated with this <see cref="CancellationTokenSource"/>.</summary>
95 /// <value>The <see cref="CancellationToken"/> associated with this <see cref="CancellationTokenSource"/>.</value>
96 /// <exception cref="ObjectDisposedException">The token source has been disposed.</exception>
97 public CancellationToken Token
102 return new CancellationToken(this);
106 internal WaitHandle WaitHandle
112 // Return the handle if it was already allocated.
113 if (_kernelEvent != null)
118 // Lazily-initialize the handle.
119 var mre = new ManualResetEvent(false);
120 if (Interlocked.CompareExchange(ref _kernelEvent, mre, null) != null)
125 // There is a race condition between checking IsCancellationRequested and setting the event.
126 // However, at this point, the kernel object definitely exists and the cases are:
127 // 1. if IsCancellationRequested = true, then we will call Set()
128 // 2. if IsCancellationRequested = false, then NotifyCancellation will see that the event exists, and will call Set().
129 if (IsCancellationRequested)
139 /// <summary>Gets the ID of the currently executing callback.</summary>
140 internal long ExecutingCallback => Volatile.Read(ref _executingCallbackId);
142 /// <summary>Initializes the <see cref="CancellationTokenSource"/>.</summary>
143 public CancellationTokenSource() => _state = NotCanceledState;
146 /// Constructs a <see cref="CancellationTokenSource"/> that will be canceled after a specified time span.
148 /// <param name="delay">The time span to wait before canceling this <see cref="CancellationTokenSource"/></param>
149 /// <exception cref="ArgumentOutOfRangeException">
150 /// The exception that is thrown when <paramref name="delay"/> is less than -1 or greater than Int32.MaxValue.
154 /// The countdown for the delay starts during the call to the constructor. When the delay expires,
155 /// the constructed <see cref="CancellationTokenSource"/> is canceled, if it has
156 /// not been canceled already.
159 /// Subsequent calls to CancelAfter will reset the delay for the constructed
160 /// <see cref="CancellationTokenSource"/>, if it has not been
161 /// canceled already.
164 public CancellationTokenSource(TimeSpan delay)
166 long totalMilliseconds = (long)delay.TotalMilliseconds;
167 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
169 throw new ArgumentOutOfRangeException(nameof(delay));
172 InitializeWithTimer((int)totalMilliseconds);
176 /// Constructs a <see cref="CancellationTokenSource"/> that will be canceled after a specified time span.
178 /// <param name="millisecondsDelay">The time span to wait before canceling this <see cref="CancellationTokenSource"/></param>
179 /// <exception cref="ArgumentOutOfRangeException">
180 /// The exception that is thrown when <paramref name="millisecondsDelay"/> is less than -1.
184 /// The countdown for the millisecondsDelay starts during the call to the constructor. When the millisecondsDelay expires,
185 /// the constructed <see cref="CancellationTokenSource"/> is canceled (if it has
186 /// not been canceled already).
189 /// Subsequent calls to CancelAfter will reset the millisecondsDelay for the constructed
190 /// <see cref="CancellationTokenSource"/>, if it has not been
191 /// canceled already.
194 public CancellationTokenSource(int millisecondsDelay)
196 if (millisecondsDelay < -1)
198 throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
201 InitializeWithTimer(millisecondsDelay);
204 /// <summary>Common initialization logic when constructing a CTS with a delay parameter</summary>
205 private void InitializeWithTimer(int millisecondsDelay)
207 _state = NotCanceledState;
208 _timer = new Timer(s_timerCallback, this, millisecondsDelay, -1);
211 /// <summary>Communicates a request for cancellation.</summary>
214 /// The associated <see cref="CancellationToken" /> will be notified of the cancellation
215 /// and will transition to a state where <see cref="CancellationToken.IsCancellationRequested"/> returns true.
216 /// Any callbacks or cancelable operations registered with the <see cref="CancellationToken"/> will be executed.
219 /// Cancelable operations and callbacks registered with the token should not throw exceptions.
220 /// However, this overload of Cancel will aggregate any exceptions thrown into a <see cref="AggregateException"/>,
221 /// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
224 /// The <see cref="ExecutionContext"/> that was captured when each callback was registered
225 /// will be reestablished when the callback is invoked.
228 /// <exception cref="AggregateException">An aggregate exception containing all the exceptions thrown
229 /// by the registered callbacks on the associated <see cref="CancellationToken"/>.</exception>
230 /// <exception cref="ObjectDisposedException">This <see cref="CancellationTokenSource"/> has been disposed.</exception>
231 public void Cancel() => Cancel(false);
233 /// <summary>Communicates a request for cancellation.</summary>
236 /// The associated <see cref="CancellationToken" /> will be notified of the cancellation and will transition to a state where
237 /// <see cref="CancellationToken.IsCancellationRequested"/> returns true. Any callbacks or cancelable operationsregistered
238 /// with the <see cref="CancellationToken"/> will be executed.
241 /// Cancelable operations and callbacks registered with the token should not throw exceptions.
242 /// If <paramref name="throwOnFirstException"/> is true, an exception will immediately propagate out of the
243 /// call to Cancel, preventing the remaining callbacks and cancelable operations from being processed.
244 /// If <paramref name="throwOnFirstException"/> is false, this overload will aggregate any
245 /// exceptions thrown into a <see cref="AggregateException"/>,
246 /// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
249 /// The <see cref="ExecutionContext"/> that was captured when each callback was registered
250 /// will be reestablished when the callback is invoked.
253 /// <param name="throwOnFirstException">Specifies whether exceptions should immediately propagate.</param>
254 /// <exception cref="AggregateException">An aggregate exception containing all the exceptions thrown
255 /// by the registered callbacks on the associated <see cref="CancellationToken"/>.</exception>
256 /// <exception cref="ObjectDisposedException">This <see cref="CancellationTokenSource"/> has been disposed.</exception>
257 public void Cancel(bool throwOnFirstException)
260 NotifyCancellation(throwOnFirstException);
263 /// <summary>Schedules a Cancel operation on this <see cref="CancellationTokenSource"/>.</summary>
264 /// <param name="delay">The time span to wait before canceling this <see cref="CancellationTokenSource"/>.
266 /// <exception cref="ObjectDisposedException">The exception thrown when this <see
267 /// cref="CancellationTokenSource"/> has been disposed.
269 /// <exception cref="ArgumentOutOfRangeException">
270 /// The exception thrown when <paramref name="delay"/> is less than -1 or
271 /// greater than Int32.MaxValue.
275 /// The countdown for the delay starts during this call. When the delay expires,
276 /// this <see cref="CancellationTokenSource"/> is canceled, if it has
277 /// not been canceled already.
280 /// Subsequent calls to CancelAfter will reset the delay for this
281 /// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
284 public void CancelAfter(TimeSpan delay)
286 long totalMilliseconds = (long)delay.TotalMilliseconds;
287 if (totalMilliseconds < -1 || totalMilliseconds > int.MaxValue)
289 throw new ArgumentOutOfRangeException(nameof(delay));
292 CancelAfter((int)totalMilliseconds);
296 /// Schedules a Cancel operation on this <see cref="CancellationTokenSource"/>.
298 /// <param name="millisecondsDelay">The time span to wait before canceling this <see
299 /// cref="CancellationTokenSource"/>.
301 /// <exception cref="ObjectDisposedException">The exception thrown when this <see
302 /// cref="CancellationTokenSource"/> has been disposed.
304 /// <exception cref="ArgumentOutOfRangeException">
305 /// The exception thrown when <paramref name="millisecondsDelay"/> is less than -1.
309 /// The countdown for the millisecondsDelay starts during this call. When the millisecondsDelay expires,
310 /// this <see cref="CancellationTokenSource"/> is canceled, if it has
311 /// not been canceled already.
314 /// Subsequent calls to CancelAfter will reset the millisecondsDelay for this
315 /// <see cref="CancellationTokenSource"/>, if it has not been
316 /// canceled already.
319 public void CancelAfter(int millisecondsDelay)
323 if (millisecondsDelay < -1)
325 throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
328 if (IsCancellationRequested)
333 // There is a race condition here as a Cancel could occur between the check of
334 // IsCancellationRequested and the creation of the timer. This is benign; in the
335 // worst case, a timer will be created that has no effect when it expires.
337 // Also, if Dispose() is called right here (after ThrowIfDisposed(), before timer
338 // creation), it would result in a leaked Timer object (at least until the timer
339 // expired and Disposed itself). But this would be considered bad behavior, as
340 // Dispose() is not thread-safe and should not be called concurrently with CancelAfter().
344 // Lazily initialize the timer in a thread-safe fashion.
345 // Initially set to "never go off" because we don't want to take a
346 // chance on a timer "losing" the initialization and then
347 // cancelling the token before it (the timer) can be disposed.
348 Timer newTimer = new Timer(s_timerCallback, this, -1, -1);
349 if (Interlocked.CompareExchange(ref _timer, newTimer, null) != null)
351 // We did not initialize the timer. Dispose the new timer.
356 // It is possible that m_timer has already been disposed, so we must do
357 // the following in a try/catch block.
360 _timer.Change(millisecondsDelay, -1);
362 catch (ObjectDisposedException)
364 // Just eat the exception. There is no other way to tell that
365 // the timer has been disposed, and even if there were, there
366 // would not be a good way to deal with the observe/dispose
373 /// <summary>Releases the resources used by this <see cref="CancellationTokenSource" />.</summary>
374 /// <remarks>This method is not thread-safe for any other concurrent calls.</remarks>
375 public void Dispose()
378 GC.SuppressFinalize(this);
382 /// Releases the unmanaged resources used by the <see cref="CancellationTokenSource" /> class and optionally releases the managed resources.
384 /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
385 protected virtual void Dispose(bool disposing)
387 if (disposing && !_disposed)
389 // We specifically tolerate that a callback can be deregistered
390 // after the CTS has been disposed and/or concurrently with cts.Dispose().
391 // This is safe without locks because Dispose doesn't interact with values
392 // in the callback partitions, only nulling out the ref to existing partitions.
394 // We also tolerate that a callback can be registered after the CTS has been
395 // disposed. This is safe because InternalRegister is tolerant
396 // of _callbackPartitions becoming null during its execution. However,
397 // we run the acceptable risk of _callbackPartitions getting reinitialized
398 // to non-null if there is a race between Dispose and Register, in which case this
399 // instance may unnecessarily hold onto a registered callback. But that's no worse
400 // than if Dispose wasn't safe to use concurrently, as Dispose would never be called,
401 // and thus no handlers would be dropped.
403 // And, we tolerate Dispose being used concurrently with Cancel. This is necessary
404 // to properly support, e.g., LinkedCancellationTokenSource, where, due to common usage patterns,
405 // it's possible for this pairing to occur with valid usage (e.g. a component accepts
406 // an external CancellationToken and uses CreateLinkedTokenSource to combine it with an
407 // internal source of cancellation, then Disposes of that linked source, which could
408 // happen at the same time the external entity is requesting cancellation).
410 _timer?.Dispose(); // Timer.Dispose is thread-safe
412 _callbackPartitions = null; // free for GC; Cancel correctly handles a null field
414 // If a kernel event was created via WaitHandle, we'd like to Dispose of it. However,
415 // we only want to do so if it's not being used by Cancel concurrently. First, we
416 // interlocked exchange it to be null, and then we check whether cancellation is currently
417 // in progress. NotifyCancellation will only try to set the event if it exists after it's
418 // transitioned to and while it's in the NotifyingState.
419 if (_kernelEvent != null)
421 ManualResetEvent mre = Interlocked.Exchange(ref _kernelEvent, null);
422 if (mre != null && _state != NotifyingState)
432 /// <summary>Throws an exception if the source has been disposed.</summary>
433 private void ThrowIfDisposed()
437 ThrowObjectDisposedException();
441 /// <summary>Throws an <see cref="ObjectDisposedException"/>. Separated out from ThrowIfDisposed to help with inlining.</summary>
442 private static void ThrowObjectDisposedException() =>
443 throw new ObjectDisposedException(null, SR.CancellationTokenSource_Disposed);
446 /// Registers a callback object. If cancellation has already occurred, the
447 /// callback will have been run by the time this method returns.
449 internal CancellationTokenRegistration InternalRegister(
450 Action<object> callback, object stateForCallback, SynchronizationContext syncContext, ExecutionContext executionContext)
452 Debug.Assert(this != s_neverCanceledSource, "This source should never be exposed via a CancellationToken.");
454 // If not canceled, register the handler; if canceled already, run the callback synchronously.
455 // This also ensures that during ExecuteCallbackHandlers() there will be no mutation of the _callbackPartitions.
456 if (!IsCancellationRequested)
458 // In order to enable code to not leak too many handlers, we allow Dispose to be called concurrently
459 // with Register. While this is not a recommended practice, consumers can and do use it this way.
460 // We don't make any guarantees about whether the CTS will hold onto the supplied callback if the CTS
461 // has already been disposed when the callback is registered, but we try not to while at the same time
462 // not paying any non-negligible overhead. The simple compromise is to check whether we're disposed
463 // (not volatile), and if we see we are, to return an empty registration. If there's a race and _disposed
464 // is false even though it's been disposed, or if the disposal request comes in after this line, we simply
465 // run the minor risk of having _callbackPartitions reinitialized (after it was cleared to null during Dispose).
468 return new CancellationTokenRegistration();
471 // Get the partitions...
472 CallbackPartition[] partitions = _callbackPartitions;
473 if (partitions == null)
475 partitions = new CallbackPartition[s_numPartitions];
476 partitions = Interlocked.CompareExchange(ref _callbackPartitions, partitions, null) ?? partitions;
479 // ...and determine which partition to use.
480 int partitionIndex = Environment.CurrentManagedThreadId & s_numPartitionsMask;
481 Debug.Assert(partitionIndex < partitions.Length, $"Expected {partitionIndex} to be less than {partitions.Length}");
482 CallbackPartition partition = partitions[partitionIndex];
483 if (partition == null)
485 partition = new CallbackPartition(this);
486 partition = Interlocked.CompareExchange(ref partitions[partitionIndex], partition, null) ?? partition;
489 // Store the callback information into the callback arrays.
492 bool lockTaken = false;
493 partition.Lock.Enter(ref lockTaken);
496 // Assign the next available unique ID.
497 id = partition.NextAvailableId++;
499 // Get a node, from the free list if possible or else a new one.
500 node = partition.FreeNodeList;
503 partition.FreeNodeList = node.Next;
504 Debug.Assert(node.Prev == null, "Nodes in the free list should all have a null Prev");
505 // node.Next will be overwritten below so no need to set it here.
509 node = new CallbackNode(partition);
512 // Configure the node.
514 node.Callback = callback;
515 node.CallbackState = stateForCallback;
516 node.ExecutionContext = executionContext;
517 node.SynchronizationContext = syncContext;
519 // Add it to the callbacks list.
520 node.Next = partition.Callbacks;
521 if (node.Next != null)
523 node.Next.Prev = node;
525 partition.Callbacks = node;
529 partition.Lock.Exit(useMemoryBarrier: false); // no check on lockTaken needed without thread aborts
532 // If cancellation hasn't been requested, return the registration.
533 // if cancellation has been requested, try to undo the registration and run the callback
534 // ourselves, but if we can't unregister it (e.g. the thread running Cancel snagged
535 // our callback for execution), return the registration so that the caller can wait
536 // for callback completion in ctr.Dispose().
537 var ctr = new CancellationTokenRegistration(id, node);
538 if (!IsCancellationRequested || !partition.Unregister(id, node))
544 // Cancellation already occurred. Run the callback on this thread and return an empty registration.
545 callback(stateForCallback);
546 return default(CancellationTokenRegistration);
549 private void NotifyCancellation(bool throwOnFirstException)
551 // If we're the first to signal cancellation, do the main extra work.
552 if (!IsCancellationRequested && Interlocked.CompareExchange(ref _state, NotifyingState, NotCanceledState) == NotCanceledState)
554 // Dispose of the timer, if any. Dispose may be running concurrently here, but Timer.Dispose is thread-safe.
557 // Set the event if it's been lazily initialized and hasn't yet been disposed of. Dispose may
558 // be running concurrently, in which case either it'll have set m_kernelEvent back to null and
559 // we won't see it here, or it'll see that we've transitioned to NOTIFYING and will skip disposing it,
560 // leaving cleanup to finalization.
561 _kernelEvent?.Set(); // update the MRE value.
563 // - late enlisters to the Canceled event will have their callbacks called immediately in the Register() methods.
564 // - Callbacks are not called inside a lock.
565 // - After transition, no more delegates will be added to the
566 // - list of handlers, and hence it can be consumed and cleared at leisure by ExecuteCallbackHandlers.
567 ExecuteCallbackHandlers(throwOnFirstException);
568 Debug.Assert(IsCancellationCompleted, "Expected cancellation to have finished");
572 /// <summary>Invoke all registered callbacks.</summary>
573 /// <remarks>The handlers are invoked synchronously in LIFO order.</remarks>
574 private void ExecuteCallbackHandlers(bool throwOnFirstException)
576 Debug.Assert(IsCancellationRequested, "ExecuteCallbackHandlers should only be called after setting IsCancellationRequested->true");
578 // Record the threadID being used for running the callbacks.
579 ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId;
581 // If there are no callbacks to run, we can safely exit. Any race conditions to lazy initialize it
582 // will see IsCancellationRequested and will then run the callback themselves.
583 CallbackPartition[] partitions = Interlocked.Exchange(ref _callbackPartitions, null);
584 if (partitions == null)
586 Interlocked.Exchange(ref _state, NotifyingCompleteState);
590 List<Exception> exceptionList = null;
593 // For each partition, and each callback in that partition, execute the associated handler.
594 // We call the delegates in LIFO order on each partition so that callbacks fire 'deepest first'.
595 // This is intended to help with nesting scenarios so that child enlisters cancel before their parents.
596 foreach (CallbackPartition partition in partitions)
598 if (partition == null)
600 // Uninitialized partition. Nothing to do.
604 // Get the callbacks from the partition, substituting in null so that anyone
605 // else coming along (e.g. CTR.Dispose) will find the callbacks gone.
607 bool lockTaken = false;
608 partition.Lock.Enter(ref lockTaken); // try/finally not needed without thread aborts
610 node = partition.Callbacks;
611 partition.Callbacks = null;
613 partition.Lock.Exit(useMemoryBarrier: false);
615 for (; node != null; node = node.Next)
617 // Publish the intended callback, to ensure ctr.Dispose can tell if a wait is necessary.
618 Volatile.Write(ref _executingCallbackId, node.Id);
620 // Invoke the callback on this thread if there's no sync context or on the
621 // target sync context if there is one.
624 if (node.SynchronizationContext != null)
626 // Transition to the target syncContext and continue there.
627 node.SynchronizationContext.Send(s =>
629 var n = (CallbackNode)s;
630 n.Partition.Source.ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId;
633 ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId; // above may have altered ThreadIDExecutingCallbacks, so reset it
637 node.ExecuteCallback();
640 catch (Exception ex) when (!throwOnFirstException)
642 // Store the exception and continue
643 (exceptionList ?? (exceptionList = new List<Exception>())).Add(ex);
650 _state = NotifyingCompleteState;
651 Volatile.Write(ref _executingCallbackId, 0);
652 Interlocked.MemoryBarrier(); // for safety, prevent reorderings crossing this point and seeing inconsistent state.
655 if (exceptionList != null)
657 Debug.Assert(exceptionList.Count > 0, $"Expected {exceptionList.Count} > 0");
658 throw new AggregateException(exceptionList);
662 /// <summary>Gets the number of callback partitions to use based on the number of cores.</summary>
663 /// <returns>A power of 2 representing the number of partitions to use.</returns>
664 private static int GetPartitionCount()
666 int procs = PlatformHelper.ProcessorCount;
668 procs > 8 ? 16 : // capped at 16 to limit memory usage on larger machines
673 Debug.Assert(count > 0 && (count & (count - 1)) == 0, $"Got {count}, but expected a power of 2");
678 /// Creates a <see cref="CancellationTokenSource"/> that will be in the canceled state
679 /// when any of the source tokens are in the canceled state.
681 /// <param name="token1">The first <see cref="CancellationToken">CancellationToken</see> to observe.</param>
682 /// <param name="token2">The second <see cref="CancellationToken">CancellationToken</see> to observe.</param>
683 /// <returns>A <see cref="CancellationTokenSource"/> that is linked
684 /// to the source tokens.</returns>
685 public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2) =>
686 !token1.CanBeCanceled ? CreateLinkedTokenSource(token2) :
687 token2.CanBeCanceled ? new Linked2CancellationTokenSource(token1, token2) :
688 (CancellationTokenSource)new Linked1CancellationTokenSource(token1);
691 /// Creates a <see cref="CancellationTokenSource"/> that will be in the canceled state
692 /// when any of the source tokens are in the canceled state.
694 /// <param name="token">The first <see cref="CancellationToken">CancellationToken</see> to observe.</param>
695 /// <returns>A <see cref="CancellationTokenSource"/> that is linked to the source tokens.</returns>
696 internal static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token) =>
697 token.CanBeCanceled ? new Linked1CancellationTokenSource(token) : new CancellationTokenSource();
700 /// Creates a <see cref="CancellationTokenSource"/> that will be in the canceled state
701 /// when any of the source tokens are in the canceled state.
703 /// <param name="tokens">The <see cref="CancellationToken">CancellationToken</see> instances to observe.</param>
704 /// <returns>A <see cref="CancellationTokenSource"/> that is linked to the source tokens.</returns>
705 /// <exception cref="System.ArgumentNullException"><paramref name="tokens"/> is null.</exception>
706 public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens)
710 throw new ArgumentNullException(nameof(tokens));
713 switch (tokens.Length)
716 throw new ArgumentException(SR.CancellationToken_CreateLinkedToken_TokensIsEmpty);
718 return CreateLinkedTokenSource(tokens[0]);
720 return CreateLinkedTokenSource(tokens[0], tokens[1]);
722 // a defensive copy is not required as the array has value-items that have only a single reference field,
723 // hence each item cannot be null itself, and reads of the payloads cannot be torn.
724 return new LinkedNCancellationTokenSource(tokens);
731 /// Wait for a single callback to complete (or, more specifically, to not be running).
732 /// It is ok to call this method if the callback has already finished.
733 /// Calling this method before the target callback has been selected for execution would be an error.
735 internal void WaitForCallbackToComplete(long id)
737 var sw = new SpinWait();
738 while (ExecutingCallback == id)
740 sw.SpinOnce(); // spin, as we assume callback execution is fast and that this situation is rare.
744 private sealed class Linked1CancellationTokenSource : CancellationTokenSource
746 private readonly CancellationTokenRegistration _reg1;
748 internal Linked1CancellationTokenSource(CancellationToken token1)
750 _reg1 = token1.InternalRegisterWithoutEC(LinkedNCancellationTokenSource.s_linkedTokenCancelDelegate, this);
753 protected override void Dispose(bool disposing)
755 if (!disposing || _disposed)
761 base.Dispose(disposing);
765 private sealed class Linked2CancellationTokenSource : CancellationTokenSource
767 private readonly CancellationTokenRegistration _reg1;
768 private readonly CancellationTokenRegistration _reg2;
770 internal Linked2CancellationTokenSource(CancellationToken token1, CancellationToken token2)
772 _reg1 = token1.InternalRegisterWithoutEC(LinkedNCancellationTokenSource.s_linkedTokenCancelDelegate, this);
773 _reg2 = token2.InternalRegisterWithoutEC(LinkedNCancellationTokenSource.s_linkedTokenCancelDelegate, this);
776 protected override void Dispose(bool disposing)
778 if (!disposing || _disposed)
785 base.Dispose(disposing);
789 private sealed class LinkedNCancellationTokenSource : CancellationTokenSource
791 internal static readonly Action<object> s_linkedTokenCancelDelegate =
792 s => ((CancellationTokenSource)s).NotifyCancellation(throwOnFirstException: false); // skip ThrowIfDisposed() check in Cancel()
793 private CancellationTokenRegistration[] m_linkingRegistrations;
795 internal LinkedNCancellationTokenSource(params CancellationToken[] tokens)
797 m_linkingRegistrations = new CancellationTokenRegistration[tokens.Length];
799 for (int i = 0; i < tokens.Length; i++)
801 if (tokens[i].CanBeCanceled)
803 m_linkingRegistrations[i] = tokens[i].InternalRegisterWithoutEC(s_linkedTokenCancelDelegate, this);
805 // Empty slots in the array will be default(CancellationTokenRegistration), which are nops to Dispose.
806 // Based on usage patterns, such occurrences should also be rare, such that it's not worth resizing
807 // the array and incurring the related costs.
811 protected override void Dispose(bool disposing)
813 if (!disposing || _disposed)
818 CancellationTokenRegistration[] linkingRegistrations = m_linkingRegistrations;
819 if (linkingRegistrations != null)
821 m_linkingRegistrations = null; // release for GC once we're done enumerating
822 for (int i = 0; i < linkingRegistrations.Length; i++)
824 linkingRegistrations[i].Dispose();
828 base.Dispose(disposing);
832 internal sealed class CallbackPartition
834 /// <summary>The associated source that owns this partition.</summary>
835 public readonly CancellationTokenSource Source;
836 /// <summary>Lock that protects all state in the partition.</summary>
837 public SpinLock Lock = new SpinLock(enableThreadOwnerTracking: false); // mutable struct; do not make this readonly
839 /// The array of callbacks registered in the partition. Slots may be empty, meaning a default value of the struct.
840 /// <see cref="NextCallbacksSlot"/> - 1 defines the last filled slot.
843 /// Initialized to an array with at least 1 slot because a partition is only ever created if we're about
844 /// to store something into it. And initialized with at most 1 slot to help optimize the common case where
845 /// there's only ever a single registration in a CTS (that and many registrations are the most common cases).
847 public CallbackNode Callbacks;
848 public CallbackNode FreeNodeList;
850 /// Every callback is assigned a unique, never-reused ID. This defines the next available ID.
852 public long NextAvailableId = 1; // avoid using 0, as that's the default long value and used to represent an empty slot
854 public CallbackPartition(CancellationTokenSource source)
856 Debug.Assert(source != null, "Expected non-null source");
860 internal bool Unregister(long id, CallbackNode node)
862 Debug.Assert(id != 0, "Expected non-zero id");
863 Debug.Assert(node != null, "Expected non-null node");
865 bool lockTaken = false;
866 Lock.Enter(ref lockTaken);
869 if (Callbacks == null || node.Id != id)
871 // Cancellation was already requested or the callback was already disposed.
872 // Even though we have the node itself, it's important to check Callbacks
873 // in order to synchronize with callback execution.
877 // Remove the registration from the list.
878 if (node.Prev != null) node.Prev.Next = node.Next;
879 if (node.Next != null) node.Next.Prev = node.Prev;
880 if (Callbacks == node) Callbacks = node.Next;
882 // Clear it out and put it on the free list
885 node.Next = FreeNodeList;
892 Lock.Exit(useMemoryBarrier: false); // no check on lockTaken needed without thread aborts
897 /// <summary>All of the state associated a registered callback, in a node that's part of a linked list of registered callbacks.</summary>
898 internal sealed class CallbackNode
900 public readonly CallbackPartition Partition;
901 public CallbackNode Prev;
902 public CallbackNode Next;
905 public Action<object> Callback;
906 public object CallbackState;
907 public ExecutionContext ExecutionContext;
908 public SynchronizationContext SynchronizationContext;
910 public CallbackNode(CallbackPartition partition)
912 Debug.Assert(partition != null, "Expected non-null partition");
913 Partition = partition;
920 CallbackState = null;
921 ExecutionContext = null;
922 SynchronizationContext = null;
925 public void ExecuteCallback()
927 if (ExecutionContext != null)
929 ExecutionContext.Run(ExecutionContext, s =>
931 CallbackNode n = (CallbackNode)s;
932 n.Callback(n.CallbackState);
937 Callback(CallbackState);