// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
namespace System.Threading
{
///
/// Represents a callback delegate that has been registered with a CancellationToken.
///
///
/// To unregister a callback, dispose the corresponding Registration instance.
///
public readonly struct CancellationTokenRegistration : IEquatable, IDisposable, IAsyncDisposable
{
private readonly long _id;
private readonly CancellationTokenSource.CallbackNode _node;
internal CancellationTokenRegistration(long id, CancellationTokenSource.CallbackNode node)
{
_id = id;
_node = node;
}
///
/// Disposes of the registration and unregisters the target callback from the associated
/// CancellationToken.
/// 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.
///
public void Dispose()
{
CancellationTokenSource.CallbackNode node = _node;
if (node != null && !node.Partition.Unregister(_id, node))
{
WaitForCallbackIfNecessary();
}
}
///
/// Disposes of the registration and unregisters the target callback from the associated
/// CancellationToken.
/// The returned 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.
///
public ValueTask DisposeAsync()
{
CancellationTokenSource.CallbackNode node = _node;
return node != null && !node.Partition.Unregister(_id, node) ?
WaitForCallbackIfNecessaryAsync() :
default;
}
///
/// Gets the with which this registration is associated. If the
/// registration isn't associated with a token (such as after the registration has been disposed),
/// this will return a default token.
///
public CancellationToken Token
{
get
{
CancellationTokenSource.CallbackNode node = _node;
return node != null ?
new CancellationToken(node.Partition.Source) : // avoid CTS.Token, which throws after disposal
default;
}
}
///
/// Disposes of the registration and unregisters the target callback from the associated
/// CancellationToken.
///
public bool Unregister()
{
CancellationTokenSource.CallbackNode node = _node;
return node != null && node.Partition.Unregister(_id, node);
}
private void WaitForCallbackIfNecessary()
{
// 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.Partition.Source;
if (source.IsCancellationRequested && // Running callbacks has commenced.
!source.IsCancellationCompleted && // Running callbacks hasn't finished.
source.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.
source.WaitForCallbackToComplete(_id);
}
}
private ValueTask WaitForCallbackIfNecessaryAsync()
{
// Same as WaitForCallbackIfNecessary, except returning a task that'll be completed when callbacks complete.
CancellationTokenSource source = _node.Partition.Source;
if (source.IsCancellationRequested && // Running callbacks has commenced.
!source.IsCancellationCompleted && // Running callbacks hasn't finished.
source.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 source.WaitForCallbackToCompleteAsync(_id);
}
// Callback is either already completed, won't execute, or the callback itself is calling this.
return default;
}
///
/// Determines whether two CancellationTokenRegistration
/// instances are equal.
///
/// The first instance.
/// The second instance.
/// True if the instances are equal; otherwise, false.
public static bool operator ==(CancellationTokenRegistration left, CancellationTokenRegistration right) => left.Equals(right);
///
/// Determines whether two CancellationTokenRegistration instances are not equal.
///
/// The first instance.
/// The second instance.
/// True if the instances are not equal; otherwise, false.
public static bool operator !=(CancellationTokenRegistration left, CancellationTokenRegistration right) => !left.Equals(right);
///
/// Determines whether the current CancellationTokenRegistration instance is equal to the
/// specified .
///
/// The other object to which to compare this instance.
/// True, if both this and are equal. False, otherwise.
/// Two CancellationTokenRegistration instances are equal if
/// they both refer to the output of a single call to the same Register method of a
/// CancellationToken.
///
public override bool Equals(object obj) => obj is CancellationTokenRegistration && Equals((CancellationTokenRegistration)obj);
///
/// Determines whether the current CancellationToken instance is equal to the
/// specified .
///
/// The other CancellationTokenRegistration to which to compare this instance.
/// True, if both this and are equal. False, otherwise.
/// Two CancellationTokenRegistration instances are equal if
/// they both refer to the output of a single call to the same Register method of a
/// CancellationToken.
///
public bool Equals(CancellationTokenRegistration other) => _node == other._node && _id == other._id;
///
/// Serves as a hash function for a CancellationTokenRegistration..
///
/// A hash code for the current CancellationTokenRegistration instance.
public override int GetHashCode() => _node != null ? _node.GetHashCode() ^ _id.GetHashCode() : _id.GetHashCode();
}
}