Re-enable StyleCop warnings SA1028 and SA1518 (trailing whitespace and blank lines...
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Threading / CancellationTokenSource.cs
blob704e2c1d747891cdef547a30a27c40135db25b43
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>
13 /// <remarks>
14 /// <para>
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.
19 /// </para>
20 /// <para>
21 /// All members of this class, except <see cref="Dispose()"/>, are thread-safe and may be used
22 /// concurrently from multiple threads.
23 /// </para>
24 /// </remarks>
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>
47 /// <remarks>
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.
51 /// </remarks>
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>
71 /// <remarks>
72 /// <para>
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.
75 /// </para>
76 /// <para>
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.
82 /// </para>
83 /// </remarks>
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
106 ThrowIfDisposed();
107 return new CancellationToken(this);
111 internal WaitHandle WaitHandle
115 ThrowIfDisposed();
117 // Return the handle if it was already allocated.
118 if (_kernelEvent != null)
120 return _kernelEvent;
123 // Lazily-initialize the handle.
124 var mre = new ManualResetEvent(false);
125 if (Interlocked.CompareExchange(ref _kernelEvent, mre, null) != null)
127 mre.Dispose();
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)
136 _kernelEvent.Set();
139 return _kernelEvent;
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;
150 /// <summary>
151 /// Constructs a <see cref="CancellationTokenSource"/> that will be canceled after a specified time span.
152 /// </summary>
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.
156 /// </exception>
157 /// <remarks>
158 /// <para>
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.
162 /// </para>
163 /// <para>
164 /// Subsequent calls to CancelAfter will reset the delay for the constructed
165 /// <see cref="CancellationTokenSource"/>, if it has not been
166 /// canceled already.
167 /// </para>
168 /// </remarks>
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);
180 /// <summary>
181 /// Constructs a <see cref="CancellationTokenSource"/> that will be canceled after a specified time span.
182 /// </summary>
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.
186 /// </exception>
187 /// <remarks>
188 /// <para>
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).
192 /// </para>
193 /// <para>
194 /// Subsequent calls to CancelAfter will reset the millisecondsDelay for the constructed
195 /// <see cref="CancellationTokenSource"/>, if it has not been
196 /// canceled already.
197 /// </para>
198 /// </remarks>
199 public CancellationTokenSource(int millisecondsDelay)
201 if (millisecondsDelay < -1)
203 throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
206 InitializeWithTimer(millisecondsDelay);
209 /// <summary>
210 /// Common initialization logic when constructing a CTS with a delay parameter.
211 /// A zero delay will result in immediate cancellation.
212 /// </summary>
213 private void InitializeWithTimer(int millisecondsDelay)
215 if (millisecondsDelay == 0)
217 _state = NotifyingCompleteState;
219 else
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
225 // that code like:
226 // CancellationToken ct = new CancellationTokenSource(timeout).Token;
227 // will successfully cancel the token after the timeout.
231 /// <summary>Communicates a request for cancellation.</summary>
232 /// <remarks>
233 /// <para>
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.
237 /// </para>
238 /// <para>
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.
242 /// </para>
243 /// <para>
244 /// The <see cref="ExecutionContext"/> that was captured when each callback was registered
245 /// will be reestablished when the callback is invoked.
246 /// </para>
247 /// </remarks>
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>
254 /// <remarks>
255 /// <para>
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.
259 /// </para>
260 /// <para>
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.
267 /// </para>
268 /// <para>
269 /// The <see cref="ExecutionContext"/> that was captured when each callback was registered
270 /// will be reestablished when the callback is invoked.
271 /// </para>
272 /// </remarks>
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)
279 ThrowIfDisposed();
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"/>.
285 /// </param>
286 /// <exception cref="ObjectDisposedException">The exception thrown when this <see
287 /// cref="CancellationTokenSource"/> has been disposed.
288 /// </exception>
289 /// <exception cref="ArgumentOutOfRangeException">
290 /// The exception thrown when <paramref name="delay"/> is less than -1 or
291 /// greater than int.MaxValue.
292 /// </exception>
293 /// <remarks>
294 /// <para>
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.
298 /// </para>
299 /// <para>
300 /// Subsequent calls to CancelAfter will reset the delay for this
301 /// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
302 /// </para>
303 /// </remarks>
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);
315 /// <summary>
316 /// Schedules a Cancel operation on this <see cref="CancellationTokenSource"/>.
317 /// </summary>
318 /// <param name="millisecondsDelay">The time span to wait before canceling this <see
319 /// cref="CancellationTokenSource"/>.
320 /// </param>
321 /// <exception cref="ObjectDisposedException">The exception thrown when this <see
322 /// cref="CancellationTokenSource"/> has been disposed.
323 /// </exception>
324 /// <exception cref="ArgumentOutOfRangeException">
325 /// The exception thrown when <paramref name="millisecondsDelay"/> is less than -1.
326 /// </exception>
327 /// <remarks>
328 /// <para>
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.
332 /// </para>
333 /// <para>
334 /// Subsequent calls to CancelAfter will reset the millisecondsDelay for this
335 /// <see cref="CancellationTokenSource"/>, if it has not been
336 /// canceled already.
337 /// </para>
338 /// </remarks>
339 public void CancelAfter(int millisecondsDelay)
341 ThrowIfDisposed();
343 if (millisecondsDelay < -1)
345 throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
348 if (IsCancellationRequested)
350 return;
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;
363 if (timer == null)
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.
374 timer.Close();
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
390 // race condition.
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()
400 Dispose(true);
401 GC.SuppressFinalize(this);
404 /// <summary>
405 /// Releases the unmanaged resources used by the <see cref="CancellationTokenSource" /> class and optionally releases the managed resources.
406 /// </summary>
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;
434 if (timer != null)
436 _timer = null;
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)
452 mre.Dispose();
456 _disposed = true;
460 /// <summary>Throws an exception if the source has been disposed.</summary>
461 private void ThrowIfDisposed()
463 if (_disposed)
465 ThrowObjectDisposedException();
469 /// <summary>Throws an <see cref="ObjectDisposedException"/>. Separated out from ThrowIfDisposed to help with inlining.</summary>
470 [DoesNotReturn]
471 private static void ThrowObjectDisposedException() =>
472 throw new ObjectDisposedException(null, SR.CancellationTokenSource_Disposed);
474 /// <summary>
475 /// Registers a callback object. If cancellation has already occurred, the
476 /// callback will have been run by the time this method returns.
477 /// </summary>
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).
495 if (_disposed)
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.
519 long id;
520 CallbackNode? node;
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;
530 if (node != null)
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.
536 else
538 node = new CallbackNode(partition);
541 // Configure the node.
542 node.Id = id;
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;
556 finally
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))
569 return ctr;
573 // Cancellation already occurred. Run the callback on this thread and return an empty registration.
574 callback(stateForCallback);
575 return default;
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;
585 if (timer != null)
587 _timer = null;
588 timer.Close();
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);
621 return;
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.
635 continue;
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.
641 while (true)
643 CallbackNode? node;
644 bool lockTaken = false;
645 partition.Lock.Enter(ref lockTaken);
648 // Pop the next registration from the callbacks list.
649 node = partition.Callbacks;
650 if (node == null)
652 // No more registrations to process.
653 break;
655 else
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.
670 node.Id = 0;
672 finally
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;
688 n.ExecuteCallback();
689 }, node);
690 ThreadIDExecutingCallbacks = Environment.CurrentManagedThreadId; // above may have altered ThreadIDExecutingCallbacks, so reset it
692 else
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).
711 finally
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;
730 int count =
731 procs > 8 ? 16 : // capped at 16 to limit memory usage on larger machines
732 procs > 4 ? 8 :
733 procs > 2 ? 4 :
734 procs > 1 ? 2 :
736 Debug.Assert(count > 0 && (count & (count - 1)) == 0, $"Got {count}, but expected a power of 2");
737 return count;
740 /// <summary>
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.
743 /// </summary>
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);
753 /// <summary>
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.
756 /// </summary>
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();
762 /// <summary>
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.
765 /// </summary>
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)
771 if (tokens == null)
773 throw new ArgumentNullException(nameof(tokens));
776 switch (tokens.Length)
778 case 0:
779 throw new ArgumentException(SR.CancellationToken_CreateLinkedToken_TokensIsEmpty);
780 case 1:
781 return CreateLinkedTokenSource(tokens[0]);
782 case 2:
783 return CreateLinkedTokenSource(tokens[0], tokens[1]);
784 default:
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);
791 /// <summary>
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.
795 /// </summary>
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.
805 /// <summary>
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.
809 /// </summary>
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)
817 return default;
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)
846 return;
849 _reg1.Dispose();
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)
869 return;
872 _reg1.Dispose();
873 _reg2.Dispose();
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)
907 return;
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");
940 Source = 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);
952 if (node.Id != id)
954 // Either:
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.
961 return false;
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;
970 else
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).
985 node.Id = 0;
986 node.Callback = null;
987 node.CallbackState = null;
988 node.ExecutionContext = null;
989 node.SynchronizationContext = null;
990 node.Prev = null;
991 node.Next = FreeNodeList;
992 FreeNodeList = node;
994 return true;
996 finally
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;
1010 public long Id;
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);
1034 }, this);
1036 else
1038 Debug.Assert(Callback != null);
1039 Callback(CallbackState);