File: src\libraries\System.Private.CoreLib\src\System\Threading\CancellationTokenRegistration.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
 
namespace System.Threading
{
    /// <summary>
    /// Represents a callback delegate that has been registered with a <see cref="CancellationToken">CancellationToken</see>.
    /// </summary>
    /// <remarks>
    /// To unregister a callback, dispose the corresponding Registration instance.
    /// </remarks>
    public readonly struct CancellationTokenRegistration : IEquatable<CancellationTokenRegistration>, IDisposable, IAsyncDisposable
    {
        private readonly long _id;
        private readonly CancellationTokenSource.CallbackNode _node;
 
        internal CancellationTokenRegistration(long id, CancellationTokenSource.CallbackNode node)
        {
            _id = id;
            _node = node;
        }
 
        /// <summary>
        /// Disposes of the registration and unregisters the target callback from the associated
        /// <see cref="CancellationToken">CancellationToken</see>.
        /// If the target callback is currently executing, this method will wait until it completes, except
        /// in the degenerate cases where a callback method unregisters itself.
        /// </summary>
        public void Dispose()
        {
            if (_node is CancellationTokenSource.CallbackNode node && !node.Registrations.Unregister(_id, node))
            {
                WaitForCallbackIfNecessary(_id, node);
 
                static void WaitForCallbackIfNecessary(long id, CancellationTokenSource.CallbackNode node)
                {
                    // We're a valid registration but we were unable to unregister, which means the callback wasn't in the list,
                    // which means either it already executed or it's currently executing. We guarantee that we will not return
                    // if the callback is being executed (assuming we are not currently called by the callback itself)
                    // We achieve this by the following rules:
                    //    1. If we are called in the context of an executing callback, no need to wait (determined by tracking callback-executor threadID)
                    //       - if the currently executing callback is this CTR, then waiting would deadlock. (We choose to return rather than deadlock)
                    //       - if not, then this CTR cannot be the one executing, hence no need to wait
                    //    2. If unregistration failed, and we are on a different thread, then the callback may be running under control of cts.Cancel()
                    //       => poll until cts.ExecutingCallback is not the one we are trying to unregister.
                    CancellationTokenSource source = node.Registrations.Source;
                    if (source.IsCancellationRequested && // Running callbacks has commenced.
                        !source.IsCancellationCompleted && // Running callbacks hasn't finished.
                        node.Registrations.ThreadIDExecutingCallbacks != Environment.CurrentManagedThreadId) // The executing thread ID is not this thread's ID.
                    {
                        // Callback execution is in progress, the executing thread is different from this thread and has taken the callback for execution
                        // so observe and wait until this target callback is no longer the executing callback.
                        node.Registrations.WaitForCallbackToComplete(id);
                    }
                }
            }
        }
 
        /// <summary>
        /// Disposes of the registration and unregisters the target callback from the associated
        /// <see cref="CancellationToken">CancellationToken</see>.
        /// The returned <see cref="ValueTask"/> will complete once the associated callback
        /// is unregistered without having executed or once it's finished executing, except
        /// in the degenerate case where the callback itself is unregistering itself.
        /// </summary>
        public ValueTask DisposeAsync()
        {
            return _node is CancellationTokenSource.CallbackNode node && !node.Registrations.Unregister(_id, node) ?
                WaitForCallbackIfNecessaryAsync(_id, node) :
                default;
 
            static ValueTask WaitForCallbackIfNecessaryAsync(long id, CancellationTokenSource.CallbackNode node)
            {
                // Same as WaitForCallbackIfNecessary, except returning a task that'll be completed when callbacks complete.
 
                CancellationTokenSource source = node.Registrations.Source;
                if (source.IsCancellationRequested && // Running callbacks has commenced.
                    !source.IsCancellationCompleted && // Running callbacks hasn't finished.
                    node.Registrations.ThreadIDExecutingCallbacks != Environment.CurrentManagedThreadId) // The executing thread ID is not this thread's ID.
                {
                    // Callback execution is in progress, the executing thread is different from this thread and has taken the callback for execution
                    // so get a task that'll complete when this target callback is no longer the executing callback.
                    return node.Registrations.WaitForCallbackToCompleteAsync(id);
                }
 
                // Callback is either already completed, won't execute, or the callback itself is calling this.
                return default;
            }
        }
 
        /// <summary>Gets the <see cref="CancellationToken"/> with which this registration is associated.</summary>
        /// <remarks>
        /// If the registration isn't associated with a token (such as for a registration returned from a call
        /// to <see cref="CancellationToken.Register"/> on a token that already had cancellation requested),
        /// this will return a default token.
        /// </remarks>
        public CancellationToken Token =>
            _node is CancellationTokenSource.CallbackNode node ?
                new CancellationToken(node.Registrations.Source) : // avoid CTS.Token, which throws after disposal
                default;
 
        /// <summary>
        /// Disposes of the registration and unregisters the target callback from the associated
        /// <see cref="CancellationToken">CancellationToken</see>.
        /// </summary>
        public bool Unregister() =>
            _node is CancellationTokenSource.CallbackNode node && node.Registrations.Unregister(_id, node);
 
        /// <summary>
        /// Determines whether two <see
        /// cref="CancellationTokenRegistration">CancellationTokenRegistration</see>
        /// instances are equal.
        /// </summary>
        /// <param name="left">The first instance.</param>
        /// <param name="right">The second instance.</param>
        /// <returns>True if the instances are equal; otherwise, false.</returns>
        public static bool operator ==(CancellationTokenRegistration left, CancellationTokenRegistration right) => left.Equals(right);
 
        /// <summary>
        /// Determines whether two <see cref="CancellationTokenRegistration">CancellationTokenRegistration</see> instances are not equal.
        /// </summary>
        /// <param name="left">The first instance.</param>
        /// <param name="right">The second instance.</param>
        /// <returns>True if the instances are not equal; otherwise, false.</returns>
        public static bool operator !=(CancellationTokenRegistration left, CancellationTokenRegistration right) => !left.Equals(right);
 
        /// <summary>
        /// Determines whether the current <see cref="CancellationTokenRegistration">CancellationTokenRegistration</see> instance is equal to the
        /// specified <see cref="object"/>.
        /// </summary>
        /// <param name="obj">The other object to which to compare this instance.</param>
        /// <returns>True, if both this and <paramref name="obj"/> are equal. False, otherwise.
        /// Two <see cref="CancellationTokenRegistration">CancellationTokenRegistration</see> instances are equal if
        /// they both refer to the output of a single call to the same Register method of a
        /// <see cref="CancellationToken">CancellationToken</see>.
        /// </returns>
        public override bool Equals([NotNullWhen(true)] object? obj) => obj is CancellationTokenRegistration other && Equals(other);
 
        /// <summary>
        /// Determines whether the current <see cref="CancellationToken">CancellationToken</see> instance is equal to the
        /// specified <see cref="object"/>.
        /// </summary>
        /// <param name="other">The other <see cref="CancellationTokenRegistration">CancellationTokenRegistration</see> to which to compare this instance.</param>
        /// <returns>True, if both this and <paramref name="other"/> are equal. False, otherwise.
        /// Two <see cref="CancellationTokenRegistration">CancellationTokenRegistration</see> instances are equal if
        /// they both refer to the output of a single call to the same Register method of a
        /// <see cref="CancellationToken">CancellationToken</see>.
        /// </returns>
        public bool Equals(CancellationTokenRegistration other) => _node == other._node && _id == other._id;
 
        /// <summary>
        /// Serves as a hash function for a <see cref="CancellationTokenRegistration">CancellationTokenRegistration.</see>.
        /// </summary>
        /// <returns>A hash code for the current <see cref="CancellationTokenRegistration">CancellationTokenRegistration</see> instance.</returns>
        public override int GetHashCode() => _node != null ? _node.GetHashCode() ^ _id.GetHashCode() : _id.GetHashCode();
    }
}