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
;
7 using System
.Diagnostics
.CodeAnalysis
;
8 using System
.Threading
.Tasks
;
10 namespace System
.Threading
12 /// <summary>Signals to a <see cref="CancellationToken"/> that it should be canceled.</summary>
15 /// <see cref="CancellationTokenSource"/> is used to instantiate a <see cref="CancellationToken"/> (via
16 /// the source's <see cref="Token">Token</see> property) that can be handed to operations that wish to be
17 /// notified of cancellation or that can be used to register asynchronous operations for cancellation. That
18 /// token may have cancellation requested by calling to the source's <see cref="Cancel()"/> method.
21 /// All members of this class, except <see cref="Dispose()"/>, are thread-safe and may be used
22 /// concurrently from multiple threads.
25 public class CancellationTokenSource
: IDisposable
27 /// <summary>A <see cref="CancellationTokenSource"/> that's already canceled.</summary>
28 internal static readonly CancellationTokenSource s_canceledSource
= new CancellationTokenSource() { _state = NotifyingCompleteState }
;
29 /// <summary>A <see cref="CancellationTokenSource"/> that's never canceled. This isn't enforced programmatically, only by usage. Do not cancel!</summary>
30 internal static readonly CancellationTokenSource s_neverCanceledSource
= new CancellationTokenSource();
32 /// <summary>Delegate used with <see cref="Timer"/> to trigger cancellation of a <see cref="CancellationTokenSource"/>.</summary>
33 private static readonly TimerCallback s_timerCallback
= obj
=>
35 Debug
.Assert(obj
is CancellationTokenSource
, $"Expected {typeof(CancellationTokenSource)}, got {obj}");
36 ((CancellationTokenSource
)obj
).NotifyCancellation(throwOnFirstException
: false); // skip ThrowIfDisposed() check in Cancel()
39 /// <summary>The number of callback partitions to use in a <see cref="CancellationTokenSource"/>. Must be a power of 2.</summary>
40 private static readonly int s_numPartitions
= GetPartitionCount();
41 /// <summary><see cref="s_numPartitions"/> - 1, used to quickly mod into <see cref="_callbackPartitions"/>.</summary>
42 private static readonly int s_numPartitionsMask
= s_numPartitions
- 1;
44 /// <summary>The current state of the CancellationTokenSource.</summary>
45 private volatile int _state
;
46 /// <summary>The ID of the thread currently executing the main body of CTS.Cancel()</summary>
48 /// This helps us to know if a call to ctr.Dispose() is running 'within' a cancellation callback.
49 /// This is updated as we move between the main thread calling cts.Cancel() and any syncContexts
50 /// that are used to actually run the callbacks.
52 private volatile int _threadIDExecutingCallbacks
= -1;
53 /// <summary>Tracks the running callback to assist ctr.Dispose() to wait for the target callback to complete.</summary>
54 private long _executingCallbackId
;
55 /// <summary>Partitions of callbacks. Split into multiple partitions to help with scalability of registering/unregistering; each is protected by its own lock.</summary>
56 private volatile CallbackPartition
?[]? _callbackPartitions
;
57 /// <summary>TimerQueueTimer used by CancelAfter and Timer-related ctors. Used instead of Timer to avoid extra allocations and because the rooted behavior is desired.</summary>
58 private volatile TimerQueueTimer
? _timer
;
59 /// <summary><see cref="System.Threading.WaitHandle"/> lazily initialized and returned from <see cref="WaitHandle"/>.</summary>
60 private volatile ManualResetEvent
? _kernelEvent
;
61 /// <summary>Whether this <see cref="CancellationTokenSource"/> has been disposed.</summary>
62 private bool _disposed
;
64 // legal values for _state
65 private const int NotCanceledState
= 1;
66 private const int NotifyingState
= 2;
67 private const int NotifyingCompleteState
= 3;
69 /// <summary>Gets whether cancellation has been requested for this <see cref="CancellationTokenSource" />.</summary>
70 /// <value>Whether cancellation has been requested for this <see cref="CancellationTokenSource" />.</value>
73 /// This property indicates whether cancellation has been requested for this token source, such as
74 /// due to a call to its <see cref="Cancel()"/> method.
77 /// If this property returns true, it only guarantees that cancellation has been requested. It does not
78 /// guarantee that every handler registered with the corresponding token has finished executing, nor
79 /// that cancellation requests have finished propagating to all registered handlers. Additional
80 /// synchronization may be required, particularly in situations where related objects are being
81 /// canceled concurrently.
84 public bool IsCancellationRequested
=> _state
>= NotifyingState
;
86 /// <summary>A simple helper to determine whether cancellation has finished.</summary>
87 internal bool IsCancellationCompleted
=> _state
== NotifyingCompleteState
;
89 /// <summary>A simple helper to determine whether disposal has occurred.</summary>
90 internal bool IsDisposed
=> _disposed
;
92 /// <summary>The ID of the thread that is running callbacks.</summary>
93 internal int ThreadIDExecutingCallbacks
95 get => _threadIDExecutingCallbacks
;
96 set => _threadIDExecutingCallbacks
= value;
99 /// <summary>Gets the <see cref="CancellationToken"/> associated with this <see cref="CancellationTokenSource"/>.</summary>
100 /// <value>The <see cref="CancellationToken"/> associated with this <see cref="CancellationTokenSource"/>.</value>
101 /// <exception cref="ObjectDisposedException">The token source has been disposed.</exception>
102 public CancellationToken Token
107 return new CancellationToken(this);
111 internal WaitHandle WaitHandle
117 // Return the handle if it was already allocated.
118 if (_kernelEvent
!= null)
123 // Lazily-initialize the handle.
124 var mre
= new ManualResetEvent(false);
125 if (Interlocked
.CompareExchange(ref _kernelEvent
, mre
, null) != null)
130 // There is a race condition between checking IsCancellationRequested and setting the event.
131 // However, at this point, the kernel object definitely exists and the cases are:
132 // 1. if IsCancellationRequested = true, then we will call Set()
133 // 2. if IsCancellationRequested = false, then NotifyCancellation will see that the event exists, and will call Set().
134 if (IsCancellationRequested
)
144 /// <summary>Gets the ID of the currently executing callback.</summary>
145 internal long ExecutingCallback
=> Volatile
.Read(ref _executingCallbackId
);
147 /// <summary>Initializes the <see cref="CancellationTokenSource"/>.</summary>
148 public CancellationTokenSource() => _state
= NotCanceledState
;
151 /// Constructs a <see cref="CancellationTokenSource"/> that will be canceled after a specified time span.
153 /// <param name="delay">The time span to wait before canceling this <see cref="CancellationTokenSource"/></param>
154 /// <exception cref="ArgumentOutOfRangeException">
155 /// The exception that is thrown when <paramref name="delay"/> is less than -1 or greater than int.MaxValue.
159 /// The countdown for the delay starts during the call to the constructor. When the delay expires,
160 /// the constructed <see cref="CancellationTokenSource"/> is canceled, if it has
161 /// not been canceled already.
164 /// Subsequent calls to CancelAfter will reset the delay for the constructed
165 /// <see cref="CancellationTokenSource"/>, if it has not been
166 /// canceled already.
169 public CancellationTokenSource(TimeSpan delay
)
171 long totalMilliseconds
= (long)delay
.TotalMilliseconds
;
172 if (totalMilliseconds
< -1 || totalMilliseconds
> int.MaxValue
)
174 throw new ArgumentOutOfRangeException(nameof(delay
));
177 InitializeWithTimer((int)totalMilliseconds
);
181 /// Constructs a <see cref="CancellationTokenSource"/> that will be canceled after a specified time span.
183 /// <param name="millisecondsDelay">The time span to wait before canceling this <see cref="CancellationTokenSource"/></param>
184 /// <exception cref="ArgumentOutOfRangeException">
185 /// The exception that is thrown when <paramref name="millisecondsDelay"/> is less than -1.
189 /// The countdown for the millisecondsDelay starts during the call to the constructor. When the millisecondsDelay expires,
190 /// the constructed <see cref="CancellationTokenSource"/> is canceled (if it has
191 /// not been canceled already).
194 /// Subsequent calls to CancelAfter will reset the millisecondsDelay for the constructed
195 /// <see cref="CancellationTokenSource"/>, if it has not been
196 /// canceled already.
199 public CancellationTokenSource(int millisecondsDelay
)
201 if (millisecondsDelay
< -1)
203 throw new ArgumentOutOfRangeException(nameof(millisecondsDelay
));
206 InitializeWithTimer(millisecondsDelay
);
210 /// Common initialization logic when constructing a CTS with a delay parameter.
211 /// A zero delay will result in immediate cancellation.
213 private void InitializeWithTimer(int millisecondsDelay
)
215 if (millisecondsDelay
== 0)
217 _state
= NotifyingCompleteState
;
221 _state
= NotCanceledState
;
222 _timer
= new TimerQueueTimer(s_timerCallback
, this, (uint)millisecondsDelay
, Timeout
.UnsignedInfinite
, flowExecutionContext
: false);
224 // The timer roots this CTS instance while it's scheduled. That is by design, so
226 // CancellationToken ct = new CancellationTokenSource(timeout).Token;
227 // will successfully cancel the token after the timeout.
231 /// <summary>Communicates a request for cancellation.</summary>
234 /// The associated <see cref="CancellationToken" /> will be notified of the cancellation
235 /// and will transition to a state where <see cref="CancellationToken.IsCancellationRequested"/> returns true.
236 /// Any callbacks or cancelable operations registered with the <see cref="CancellationToken"/> will be executed.
239 /// Cancelable operations and callbacks registered with the token should not throw exceptions.
240 /// However, this overload of Cancel will aggregate any exceptions thrown into a <see cref="AggregateException"/>,
241 /// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
244 /// The <see cref="ExecutionContext"/> that was captured when each callback was registered
245 /// will be reestablished when the callback is invoked.
248 /// <exception cref="AggregateException">An aggregate exception containing all the exceptions thrown
249 /// by the registered callbacks on the associated <see cref="CancellationToken"/>.</exception>
250 /// <exception cref="ObjectDisposedException">This <see cref="CancellationTokenSource"/> has been disposed.</exception>
251 public void Cancel() => Cancel(false);
253 /// <summary>Communicates a request for cancellation.</summary>
256 /// The associated <see cref="CancellationToken" /> will be notified of the cancellation and will transition to a state where
257 /// <see cref="CancellationToken.IsCancellationRequested"/> returns true. Any callbacks or cancelable operationsregistered
258 /// with the <see cref="CancellationToken"/> will be executed.
261 /// Cancelable operations and callbacks registered with the token should not throw exceptions.
262 /// If <paramref name="throwOnFirstException"/> is true, an exception will immediately propagate out of the
263 /// call to Cancel, preventing the remaining callbacks and cancelable operations from being processed.
264 /// If <paramref name="throwOnFirstException"/> is false, this overload will aggregate any
265 /// exceptions thrown into a <see cref="AggregateException"/>,
266 /// such that one callback throwing an exception will not prevent other registered callbacks from being executed.
269 /// The <see cref="ExecutionContext"/> that was captured when each callback was registered
270 /// will be reestablished when the callback is invoked.
273 /// <param name="throwOnFirstException">Specifies whether exceptions should immediately propagate.</param>
274 /// <exception cref="AggregateException">An aggregate exception containing all the exceptions thrown
275 /// by the registered callbacks on the associated <see cref="CancellationToken"/>.</exception>
276 /// <exception cref="ObjectDisposedException">This <see cref="CancellationTokenSource"/> has been disposed.</exception>
277 public void Cancel(bool throwOnFirstException
)
280 NotifyCancellation(throwOnFirstException
);
283 /// <summary>Schedules a Cancel operation on this <see cref="CancellationTokenSource"/>.</summary>
284 /// <param name="delay">The time span to wait before canceling this <see cref="CancellationTokenSource"/>.
286 /// <exception cref="ObjectDisposedException">The exception thrown when this <see
287 /// cref="CancellationTokenSource"/> has been disposed.
289 /// <exception cref="ArgumentOutOfRangeException">
290 /// The exception thrown when <paramref name="delay"/> is less than -1 or
291 /// greater than int.MaxValue.
295 /// The countdown for the delay starts during this call. When the delay expires,
296 /// this <see cref="CancellationTokenSource"/> is canceled, if it has
297 /// not been canceled already.
300 /// Subsequent calls to CancelAfter will reset the delay for this
301 /// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
304 public void CancelAfter(TimeSpan delay
)
306 long totalMilliseconds
= (long)delay
.TotalMilliseconds
;
307 if (totalMilliseconds
< -1 || totalMilliseconds
> int.MaxValue
)
309 throw new ArgumentOutOfRangeException(nameof(delay
));
312 CancelAfter((int)totalMilliseconds
);
316 /// Schedules a Cancel operation on this <see cref="CancellationTokenSource"/>.
318 /// <param name="millisecondsDelay">The time span to wait before canceling this <see
319 /// cref="CancellationTokenSource"/>.
321 /// <exception cref="ObjectDisposedException">The exception thrown when this <see
322 /// cref="CancellationTokenSource"/> has been disposed.
324 /// <exception cref="ArgumentOutOfRangeException">
325 /// The exception thrown when <paramref name="millisecondsDelay"/> is less than -1.
329 /// The countdown for the millisecondsDelay starts during this call. When the millisecondsDelay expires,
330 /// this <see cref="CancellationTokenSource"/> is canceled, if it has
331 /// not been canceled already.
334 /// Subsequent calls to CancelAfter will reset the millisecondsDelay for this
335 /// <see cref="CancellationTokenSource"/>, if it has not been
336 /// canceled already.
339 public void CancelAfter(int millisecondsDelay
)
343 if (millisecondsDelay
< -1)
345 throw new ArgumentOutOfRangeException(nameof(millisecondsDelay
));
348 if (IsCancellationRequested
)
353 // There is a race condition here as a Cancel could occur between the check of
354 // IsCancellationRequested and the creation of the timer. This is benign; in the
355 // worst case, a timer will be created that has no effect when it expires.
357 // Also, if Dispose() is called right here (after ThrowIfDisposed(), before timer
358 // creation), it would result in a leaked Timer object (at least until the timer
359 // expired and Disposed itself). But this would be considered bad behavior, as
360 // Dispose() is not thread-safe and should not be called concurrently with CancelAfter().
362 TimerQueueTimer
? timer
= _timer
;
365 // Lazily initialize the timer in a thread-safe fashion.
366 // Initially set to "never go off" because we don't want to take a
367 // chance on a timer "losing" the initialization and then
368 // cancelling the token before it (the timer) can be disposed.
369 timer
= new TimerQueueTimer(s_timerCallback
, this, Timeout
.UnsignedInfinite
, Timeout
.UnsignedInfinite
, flowExecutionContext
: false);
370 TimerQueueTimer
? currentTimer
= Interlocked
.CompareExchange(ref _timer
, timer
, null);
371 if (currentTimer
!= null)
373 // We did not initialize the timer. Dispose the new timer.
375 timer
= currentTimer
;
379 // It is possible that _timer has already been disposed, so we must do
380 // the following in a try/catch block.
383 timer
.Change((uint)millisecondsDelay
, Timeout
.UnsignedInfinite
);
385 catch (ObjectDisposedException
)
387 // Just eat the exception. There is no other way to tell that
388 // the timer has been disposed, and even if there were, there
389 // would not be a good way to deal with the observe/dispose
396 /// <summary>Releases the resources used by this <see cref="CancellationTokenSource" />.</summary>
397 /// <remarks>This method is not thread-safe for any other concurrent calls.</remarks>
398 public void Dispose()
401 GC
.SuppressFinalize(this);
405 /// Releases the unmanaged resources used by the <see cref="CancellationTokenSource" /> class and optionally releases the managed resources.
407 /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
408 protected virtual void Dispose(bool disposing
)
410 if (disposing
&& !_disposed
)
412 // We specifically tolerate that a callback can be unregistered
413 // after the CTS has been disposed and/or concurrently with cts.Dispose().
414 // This is safe without locks because Dispose doesn't interact with values
415 // in the callback partitions, only nulling out the ref to existing partitions.
417 // We also tolerate that a callback can be registered after the CTS has been
418 // disposed. This is safe because InternalRegister is tolerant
419 // of _callbackPartitions becoming null during its execution. However,
420 // we run the acceptable risk of _callbackPartitions getting reinitialized
421 // to non-null if there is a race between Dispose and Register, in which case this
422 // instance may unnecessarily hold onto a registered callback. But that's no worse
423 // than if Dispose wasn't safe to use concurrently, as Dispose would never be called,
424 // and thus no handlers would be dropped.
426 // And, we tolerate Dispose being used concurrently with Cancel. This is necessary
427 // to properly support, e.g., LinkedCancellationTokenSource, where, due to common usage patterns,
428 // it's possible for this pairing to occur with valid usage (e.g. a component accepts
429 // an external CancellationToken and uses CreateLinkedTokenSource to combine it with an
430 // internal source of cancellation, then Disposes of that linked source, which could
431 // happen at the same time the external entity is requesting cancellation).
433 TimerQueueTimer
? timer
= _timer
;
437 timer
.Close(); // TimerQueueTimer.Close is thread-safe
440 _callbackPartitions
= null; // free for GC; Cancel correctly handles a null field
442 // If a kernel event was created via WaitHandle, we'd like to Dispose of it. However,
443 // we only want to do so if it's not being used by Cancel concurrently. First, we
444 // interlocked exchange it to be null, and then we check whether cancellation is currently
445 // in progress. NotifyCancellation will only try to set the event if it exists after it's
446 // transitioned to and while it's in the NotifyingState.
447 if (_kernelEvent
!= null)
449 ManualResetEvent
? mre
= Interlocked
.Exchange
<ManualResetEvent
?>(ref _kernelEvent
!, null);
450 if (mre
!= null && _state
!= NotifyingState
)
460 /// <summary>Throws an exception if the source has been disposed.</summary>
461 private void ThrowIfDisposed()
465 ThrowObjectDisposedException();
469 /// <summary>Throws an <see cref="ObjectDisposedException"/>. Separated out from ThrowIfDisposed to help with inlining.</summary>
471 private static void ThrowObjectDisposedException() =>
472 throw new ObjectDisposedException(null, SR
.CancellationTokenSource_Disposed
);
475 /// Registers a callback object. If cancellation has already occurred, the
476 /// callback will have been run by the time this method returns.
478 internal CancellationTokenRegistration
InternalRegister(
479 Action
<object?> callback
, object? stateForCallback
, SynchronizationContext
? syncContext
, ExecutionContext
? executionContext
)
481 Debug
.Assert(this != s_neverCanceledSource
, "This source should never be exposed via a CancellationToken.");
483 // If not canceled, register the handler; if canceled already, run the callback synchronously.
484 // This also ensures that during ExecuteCallbackHandlers() there will be no mutation of the _callbackPartitions.
485 if (!IsCancellationRequested
)
487 // In order to enable code to not leak too many handlers, we allow Dispose to be called concurrently
488 // with Register. While this is not a recommended practice, consumers can and do use it this way.
489 // We don't make any guarantees about whether the CTS will hold onto the supplied callback if the CTS
490 // has already been disposed when the callback is registered, but we try not to while at the same time
491 // not paying any non-negligible overhead. The simple compromise is to check whether we're disposed
492 // (not volatile), and if we see we are, to return an empty registration. If there's a race and _disposed
493 // is false even though it's been disposed, or if the disposal request comes in after this line, we simply
494 // run the minor risk of having _callbackPartitions reinitialized (after it was cleared to null during Dispose).
497 return new CancellationTokenRegistration();
500 // Get the partitions...
501 CallbackPartition
?[]? partitions
= _callbackPartitions
;
502 if (partitions
== null)
504 partitions
= new CallbackPartition
[s_numPartitions
];
505 partitions
= Interlocked
.CompareExchange(ref _callbackPartitions
, partitions
, null) ?? partitions
;
508 // ...and determine which partition to use.
509 int partitionIndex
= Environment
.CurrentManagedThreadId
& s_numPartitionsMask
;
510 Debug
.Assert(partitionIndex
< partitions
.Length
, $"Expected {partitionIndex} to be less than {partitions.Length}");
511 CallbackPartition
? partition
= partitions
[partitionIndex
];
512 if (partition
== null)
514 partition
= new CallbackPartition(this);
515 partition
= Interlocked
.CompareExchange(ref partitions
[partitionIndex
], partition
, null) ?? partition
;
518 // Store the callback information into the callback arrays.
521 bool lockTaken
= false;
522 partition
.Lock
.Enter(ref lockTaken
);
525 // Assign the next available unique ID.
526 id
= partition
.NextAvailableId
++;
528 // Get a node, from the free list if possible or else a new one.
529 node
= partition
.FreeNodeList
;
532 partition
.FreeNodeList
= node
.Next
;
533 Debug
.Assert(node
.Prev
== null, "Nodes in the free list should all have a null Prev");
534 // node.Next will be overwritten below so no need to set it here.
538 node
= new CallbackNode(partition
);
541 // Configure the node.
543 node
.Callback
= callback
;
544 node
.CallbackState
= stateForCallback
;
545 node
.ExecutionContext
= executionContext
;
546 node
.SynchronizationContext
= syncContext
;
548 // Add it to the callbacks list.
549 node
.Next
= partition
.Callbacks
;
550 if (node
.Next
!= null)
552 node
.Next
.Prev
= node
;
554 partition
.Callbacks
= node
;
558 partition
.Lock
.Exit(useMemoryBarrier
: false); // no check on lockTaken needed without thread aborts
561 // If cancellation hasn't been requested, return the registration.
562 // if cancellation has been requested, try to undo the registration and run the callback
563 // ourselves, but if we can't unregister it (e.g. the thread running Cancel snagged
564 // our callback for execution), return the registration so that the caller can wait
565 // for callback completion in ctr.Dispose().
566 var ctr
= new CancellationTokenRegistration(id
, node
);
567 if (!IsCancellationRequested
|| !partition
.Unregister(id
, node
))
573 // Cancellation already occurred. Run the callback on this thread and return an empty registration.
574 callback(stateForCallback
);
578 private void NotifyCancellation(bool throwOnFirstException
)
580 // If we're the first to signal cancellation, do the main extra work.
581 if (!IsCancellationRequested
&& Interlocked
.CompareExchange(ref _state
, NotifyingState
, NotCanceledState
) == NotCanceledState
)
583 // Dispose of the timer, if any. Dispose may be running concurrently here, but TimerQueueTimer.Close is thread-safe.
584 TimerQueueTimer
? timer
= _timer
;
591 // Set the event if it's been lazily initialized and hasn't yet been disposed of. Dispose may
592 // be running concurrently, in which case either it'll have set m_kernelEvent back to null and
593 // we won't see it here, or it'll see that we've transitioned to NOTIFYING and will skip disposing it,
594 // leaving cleanup to finalization.
595 _kernelEvent
?.Set(); // update the MRE value.
597 // - late enlisters to the Canceled event will have their callbacks called immediately in the Register() methods.
598 // - Callbacks are not called inside a lock.
599 // - After transition, no more delegates will be added to the
600 // - list of handlers, and hence it can be consumed and cleared at leisure by ExecuteCallbackHandlers.
601 ExecuteCallbackHandlers(throwOnFirstException
);
602 Debug
.Assert(IsCancellationCompleted
, "Expected cancellation to have finished");
606 /// <summary>Invoke all registered callbacks.</summary>
607 /// <remarks>The handlers are invoked synchronously in LIFO order.</remarks>
608 private void ExecuteCallbackHandlers(bool throwOnFirstException
)
610 Debug
.Assert(IsCancellationRequested
, "ExecuteCallbackHandlers should only be called after setting IsCancellationRequested->true");
612 // Record the threadID being used for running the callbacks.
613 ThreadIDExecutingCallbacks
= Environment
.CurrentManagedThreadId
;
615 // If there are no callbacks to run, we can safely exit. Any race conditions to lazy initialize it
616 // will see IsCancellationRequested and will then run the callback themselves.
617 CallbackPartition
?[]? partitions
= Interlocked
.Exchange(ref _callbackPartitions
, null);
618 if (partitions
== null)
620 Interlocked
.Exchange(ref _state
, NotifyingCompleteState
);
624 List
<Exception
>? exceptionList
= null;
627 // For each partition, and each callback in that partition, execute the associated handler.
628 // We call the delegates in LIFO order on each partition so that callbacks fire 'deepest first'.
629 // This is intended to help with nesting scenarios so that child enlisters cancel before their parents.
630 foreach (CallbackPartition
? partition
in partitions
)
632 if (partition
== null)
634 // Uninitialized partition. Nothing to do.
638 // Iterate through all nodes in the partition. We remove each node prior
639 // to processing it. This allows for unregistration of subsequent registrations
640 // to still be effective even as other registrations are being invoked.
644 bool lockTaken
= false;
645 partition
.Lock
.Enter(ref lockTaken
);
648 // Pop the next registration from the callbacks list.
649 node
= partition
.Callbacks
;
652 // No more registrations to process.
657 Debug
.Assert(node
.Prev
== null);
658 if (node
.Next
!= null) node
.Next
.Prev
= null;
659 partition
.Callbacks
= node
.Next
;
662 // Publish the intended callback ID, to ensure ctr.Dispose can tell if a wait is necessary.
663 // This write happens while the lock is held so that Dispose is either able to successfully
664 // unregister or is guaranteed to see an accurate executing callback ID, since it takes
665 // the same lock to remove the node from the callback list.
666 _executingCallbackId
= node
.Id
;
668 // Now that we've grabbed the Id, reset the node's Id to 0. This signals
669 // to code unregistering that the node is no longer associated with a callback.
674 partition
.Lock
.Exit(useMemoryBarrier
: false); // no check on lockTaken needed without thread aborts
677 // Invoke the callback on this thread if there's no sync context or on the
678 // target sync context if there is one.
681 if (node
.SynchronizationContext
!= null)
683 // Transition to the target syncContext and continue there.
684 node
.SynchronizationContext
.Send(s
=>
686 var n
= (CallbackNode
)s
!;
687 n
.Partition
.Source
.ThreadIDExecutingCallbacks
= Environment
.CurrentManagedThreadId
;
690 ThreadIDExecutingCallbacks
= Environment
.CurrentManagedThreadId
; // above may have altered ThreadIDExecutingCallbacks, so reset it
694 node
.ExecuteCallback();
697 catch (Exception ex
) when (!throwOnFirstException
)
699 // Store the exception and continue
700 (exceptionList
?? (exceptionList
= new List
<Exception
>())).Add(ex
);
703 // Drop the node. While we could add it to the free list, doing so has cost (we'd need to take the lock again)
704 // and very limited value. Since a source can only be canceled once, and after it's canceled registrations don't
705 // need nodes, the only benefit to putting this on the free list would be if Register raced with cancellation
706 // occurring, such that it could have used this free node but would instead need to allocate a new node (if
707 // there wasn't another free node available).
713 _state
= NotifyingCompleteState
;
714 Volatile
.Write(ref _executingCallbackId
, 0);
715 Interlocked
.MemoryBarrier(); // for safety, prevent reorderings crossing this point and seeing inconsistent state.
718 if (exceptionList
!= null)
720 Debug
.Assert(exceptionList
.Count
> 0, $"Expected {exceptionList.Count} > 0");
721 throw new AggregateException(exceptionList
);
725 /// <summary>Gets the number of callback partitions to use based on the number of cores.</summary>
726 /// <returns>A power of 2 representing the number of partitions to use.</returns>
727 private static int GetPartitionCount()
729 int procs
= PlatformHelper
.ProcessorCount
;
731 procs
> 8 ? 16 : // capped at 16 to limit memory usage on larger machines
736 Debug
.Assert(count
> 0 && (count
& (count
- 1)) == 0, $"Got {count}, but expected a power of 2");
741 /// Creates a <see cref="CancellationTokenSource"/> that will be in the canceled state
742 /// when any of the source tokens are in the canceled state.
744 /// <param name="token1">The first <see cref="CancellationToken">CancellationToken</see> to observe.</param>
745 /// <param name="token2">The second <see cref="CancellationToken">CancellationToken</see> to observe.</param>
746 /// <returns>A <see cref="CancellationTokenSource"/> that is linked
747 /// to the source tokens.</returns>
748 public static CancellationTokenSource
CreateLinkedTokenSource(CancellationToken token1
, CancellationToken token2
) =>
749 !token1
.CanBeCanceled
? CreateLinkedTokenSource(token2
) :
750 token2
.CanBeCanceled
? new Linked2CancellationTokenSource(token1
, token2
) :
751 (CancellationTokenSource
)new Linked1CancellationTokenSource(token1
);
754 /// Creates a <see cref="CancellationTokenSource"/> that will be in the canceled state
755 /// when any of the source tokens are in the canceled state.
757 /// <param name="token">The first <see cref="CancellationToken">CancellationToken</see> to observe.</param>
758 /// <returns>A <see cref="CancellationTokenSource"/> that is linked to the source tokens.</returns>
759 internal static CancellationTokenSource
CreateLinkedTokenSource(CancellationToken token
) =>
760 token
.CanBeCanceled
? new Linked1CancellationTokenSource(token
) : new CancellationTokenSource();
763 /// Creates a <see cref="CancellationTokenSource"/> that will be in the canceled state
764 /// when any of the source tokens are in the canceled state.
766 /// <param name="tokens">The <see cref="CancellationToken">CancellationToken</see> instances to observe.</param>
767 /// <returns>A <see cref="CancellationTokenSource"/> that is linked to the source tokens.</returns>
768 /// <exception cref="System.ArgumentNullException"><paramref name="tokens"/> is null.</exception>
769 public static CancellationTokenSource
CreateLinkedTokenSource(params CancellationToken
[] tokens
)
773 throw new ArgumentNullException(nameof(tokens
));
776 switch (tokens
.Length
)
779 throw new ArgumentException(SR
.CancellationToken_CreateLinkedToken_TokensIsEmpty
);
781 return CreateLinkedTokenSource(tokens
[0]);
783 return CreateLinkedTokenSource(tokens
[0], tokens
[1]);
785 // a defensive copy is not required as the array has value-items that have only a single reference field,
786 // hence each item cannot be null itself, and reads of the payloads cannot be torn.
787 return new LinkedNCancellationTokenSource(tokens
);
792 /// Wait for a single callback to complete (or, more specifically, to not be running).
793 /// It is ok to call this method if the callback has already finished.
794 /// Calling this method before the target callback has been selected for execution would be an error.
796 internal void WaitForCallbackToComplete(long id
)
798 var sw
= new SpinWait();
799 while (ExecutingCallback
== id
)
801 sw
.SpinOnce(); // spin, as we assume callback execution is fast and that this situation is rare.
806 /// Asynchronously wait for a single callback to complete (or, more specifically, to not be running).
807 /// It is ok to call this method if the callback has already finished.
808 /// Calling this method before the target callback has been selected for execution would be an error.
810 internal ValueTask
WaitForCallbackToCompleteAsync(long id
)
812 // If the currently executing callback is not the target one, then the target one has already
813 // completed and we can simply return. This should be the most common case, as the caller
814 // calls if we're currently canceling but doesn't know what callback is running, if any.
815 if (ExecutingCallback
!= id
)
820 // The specified callback is actually running: queue a task that'll poll for the currently
821 // executing callback to complete. In general scheduling such a work item that polls is a really
822 // unfortunate thing to do. However, we expect this to be a rare case (disposing while the associated
823 // callback is running), and brief when it happens (so the polling will be minimal), and making
824 // this work with a callback mechanism will add additional cost to other more common cases.
825 return new ValueTask(Task
.Factory
.StartNew(s
=>
827 Debug
.Assert(s
is Tuple
<CancellationTokenSource
, long>);
828 var state
= (Tuple
<CancellationTokenSource
, long>)s
;
829 state
.Item1
.WaitForCallbackToComplete(state
.Item2
);
830 }, Tuple
.Create(this, id
), CancellationToken
.None
, TaskCreationOptions
.None
, TaskScheduler
.Default
));
833 private sealed class Linked1CancellationTokenSource
: CancellationTokenSource
835 private readonly CancellationTokenRegistration _reg1
;
837 internal Linked1CancellationTokenSource(CancellationToken token1
)
839 _reg1
= token1
.UnsafeRegister(LinkedNCancellationTokenSource
.s_linkedTokenCancelDelegate
, this);
842 protected override void Dispose(bool disposing
)
844 if (!disposing
|| _disposed
)
850 base.Dispose(disposing
);
854 private sealed class Linked2CancellationTokenSource
: CancellationTokenSource
856 private readonly CancellationTokenRegistration _reg1
;
857 private readonly CancellationTokenRegistration _reg2
;
859 internal Linked2CancellationTokenSource(CancellationToken token1
, CancellationToken token2
)
861 _reg1
= token1
.UnsafeRegister(LinkedNCancellationTokenSource
.s_linkedTokenCancelDelegate
, this);
862 _reg2
= token2
.UnsafeRegister(LinkedNCancellationTokenSource
.s_linkedTokenCancelDelegate
, this);
865 protected override void Dispose(bool disposing
)
867 if (!disposing
|| _disposed
)
874 base.Dispose(disposing
);
878 private sealed class LinkedNCancellationTokenSource
: CancellationTokenSource
880 internal static readonly Action
<object?> s_linkedTokenCancelDelegate
= s
=>
882 Debug
.Assert(s
is CancellationTokenSource
, $"Expected {typeof(CancellationTokenSource)}, got {s}");
883 ((CancellationTokenSource
)s
).NotifyCancellation(throwOnFirstException
: false); // skip ThrowIfDisposed() check in Cancel()
885 private CancellationTokenRegistration
[]? _linkingRegistrations
;
887 internal LinkedNCancellationTokenSource(params CancellationToken
[] tokens
)
889 _linkingRegistrations
= new CancellationTokenRegistration
[tokens
.Length
];
891 for (int i
= 0; i
< tokens
.Length
; i
++)
893 if (tokens
[i
].CanBeCanceled
)
895 _linkingRegistrations
[i
] = tokens
[i
].UnsafeRegister(s_linkedTokenCancelDelegate
, this);
897 // Empty slots in the array will be default(CancellationTokenRegistration), which are nops to Dispose.
898 // Based on usage patterns, such occurrences should also be rare, such that it's not worth resizing
899 // the array and incurring the related costs.
903 protected override void Dispose(bool disposing
)
905 if (!disposing
|| _disposed
)
910 CancellationTokenRegistration
[]? linkingRegistrations
= _linkingRegistrations
;
911 if (linkingRegistrations
!= null)
913 _linkingRegistrations
= null; // release for GC once we're done enumerating
914 for (int i
= 0; i
< linkingRegistrations
.Length
; i
++)
916 linkingRegistrations
[i
].Dispose();
920 base.Dispose(disposing
);
924 internal sealed class CallbackPartition
926 /// <summary>The associated source that owns this partition.</summary>
927 public readonly CancellationTokenSource Source
;
928 /// <summary>Lock that protects all state in the partition.</summary>
929 public SpinLock Lock
= new SpinLock(enableThreadOwnerTracking
: false); // mutable struct; do not make this readonly
930 /// <summary>Doubly-linked list of callbacks registered with the partition. Callbacks are removed during unregistration and as they're invoked.</summary>
931 public CallbackNode
? Callbacks
;
932 /// <summary>Singly-linked list of free nodes that can be used for subsequent callback registrations.</summary>
933 public CallbackNode
? FreeNodeList
;
934 /// <summary>Every callback is assigned a unique, never-reused ID. This defines the next available ID.</summary>
935 public long NextAvailableId
= 1; // avoid using 0, as that's the default long value and used to represent an empty node
937 public CallbackPartition(CancellationTokenSource source
)
939 Debug
.Assert(source
!= null, "Expected non-null source");
943 internal bool Unregister(long id
, CallbackNode node
)
945 Debug
.Assert(id
!= 0, "Expected non-zero id");
946 Debug
.Assert(node
!= null, "Expected non-null node");
948 bool lockTaken
= false;
949 Lock
.Enter(ref lockTaken
);
955 // - The callback is currently or has already been invoked, in which case node.Id
956 // will no longer equal the assigned id, as it will have transitioned to 0.
957 // - The registration was already disposed of, in which case node.Id will similarly
958 // no longer equal the assigned id, as it will have transitioned to 0 and potentially
959 // then to another (larger) value when reused for a new registration.
960 // In either case, there's nothing to unregister.
964 // The registration must still be in the callbacks list. Remove it.
965 if (Callbacks
== node
)
967 Debug
.Assert(node
.Prev
== null);
968 Callbacks
= node
.Next
;
972 Debug
.Assert(node
.Prev
!= null);
973 node
.Prev
.Next
= node
.Next
;
976 if (node
.Next
!= null)
978 node
.Next
.Prev
= node
.Prev
;
981 // Clear out the now unused node and put it on the singly-linked free list.
982 // The only field we don't clear out is the associated Partition, as that's fixed
983 // throughout the nodes lifetime, regardless of how many times its reused by
984 // the same partition (it's never used on a different partition).
986 node
.Callback
= null;
987 node
.CallbackState
= null;
988 node
.ExecutionContext
= null;
989 node
.SynchronizationContext
= null;
991 node
.Next
= FreeNodeList
;
998 Lock
.Exit(useMemoryBarrier
: false); // no check on lockTaken needed without thread aborts
1003 /// <summary>All of the state associated a registered callback, in a node that's part of a linked list of registered callbacks.</summary>
1004 internal sealed class CallbackNode
1006 public readonly CallbackPartition Partition
;
1007 public CallbackNode
? Prev
;
1008 public CallbackNode
? Next
;
1011 public Action
<object?>? Callback
;
1012 public object? CallbackState
;
1013 public ExecutionContext
? ExecutionContext
;
1014 public SynchronizationContext
? SynchronizationContext
;
1016 public CallbackNode(CallbackPartition partition
)
1018 Debug
.Assert(partition
!= null, "Expected non-null partition");
1019 Partition
= partition
;
1022 public void ExecuteCallback()
1024 ExecutionContext
? context
= ExecutionContext
;
1025 if (context
!= null)
1027 ExecutionContext
.RunInternal(context
, s
=>
1029 Debug
.Assert(s
is CallbackNode
, $"Expected {typeof(CallbackNode)}, got {s}");
1030 CallbackNode n
= (CallbackNode
)s
;
1032 Debug
.Assert(n
.Callback
!= null);
1033 n
.Callback(n
.CallbackState
);
1038 Debug
.Assert(Callback
!= null);
1039 Callback(CallbackState
);