SoFunction
Updated on 2025-03-06

Using examples to briefly explain C# cancel token CancellationTokenSource

Preface

I believe that when you use C# for development, especially when using asynchronous scenarios, you will more or less come into contact with CancellationTokenSource. You can tell that it is related to canceling asynchronous tasks by looking at the name, and you can tell that the famous CancellationToken is produced by it. I don’t know if I don’t look at it, I’m shocked when I see it. It is effective in canceling asynchronous tasks, asynchronous notifications, etc., not only easy to use but also powerful enough. Whether it is Microsoft's underlying class library or open source projects related to Task, it can basically be seen. In recent years, Microsoft has also attached great importance to asynchronous operations in the framework, especially in .NET Core, you can basically see the CancellationTokenSource where the Task is seen. This time we take a learning attitude to unveil its mystery.

Simple example

I believe that many students are already very familiar with the basic use of CancellationTokenSource. However, in order to bring everyone into the rhythm of the article, we still plan to show a few basic operations first so that everyone can find their feelings and return to that familiar era.

Basic Operation

First present the most basic operation.

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = ;
(() => ("Canceled???"));
(() => ("Canceled!!!"));
(state => ($"Canceled。。。{state}"),"Ahhhh");
("Do something else, and then canceled.");
();

This operation is the easiest operation. We mentioned above that CancellationTokenSource is used to produce CancellationTokens. It can also be said that CancellationToken is the manifestation of CancellationTokenSource. We will know why we say this when we look at the source code later. Here we register several operations for CancellationToken, and then use the Cancel method of CancellationTokenSource to cancel the operation. At this time, the console will print the result as follows

Do something else and cancel.
Cancel. . . Ahhhh
Canceled! ! !
Canceled? ? ?

Through the simple example above, you should understand its simple use very easily.

Cancel regularly

Sometimes we may need to timeout operations, for example, I don’t want to wait all the time, but when I reach a fixed time, I have to cancel the operation. At this time, we can use the constructor of CancellationTokenSource to give a limited time. After this time, the CancellationTokenSource will be cancelled. The operation is as follows

//Cancel after setting 3000 milliseconds (i.e. 3 seconds)CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000);
CancellationToken cancellationToken = ;
(() => ("I've been cancelled."));
("Wait for five seconds first.");
await (5000);
("Cancel manually.")
();

Then the result of printing on the console looks like this, which really implements the built-in timeout operation for us.

Wait for five seconds first.
I was cancelled.
Cancel manually.

The above writing method is to set a timeout waiting when constructing a CancellationTokenSource. There is another writing method equivalent to this writing method. It uses the CancelAfter method, which is used as follows

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
(() => ("I've been cancelled."));
//Cancel after five seconds(5000);
("It won't block, I will execute.");

This operation is also a timed cancellation operation. It should be noted that the CancelAfter method does not block execution, so the print result is

It won't block, I will execute it.
I was cancelled.

Related Cancel

Sometimes this scenario is like this, we set up a set of associated CancellationTokenSources. What we expect is that as long as any CancellationTokenSource in this group is cancelled, the associated CancellationTokenSource will be cancelled. To put it simply, as long as one of us is gone, you can also be gone. The specific implementation method is like this

//Declare several CancellationTokenSourcesCancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationTokenSource tokenSource2 = new CancellationTokenSource();
CancellationTokenSource tokenSource3 = new CancellationTokenSource();

(() => ("tokenSource2 was cancelled"));

//Create an associated CancellationTokenSourceCancellationTokenSource tokenSourceNew = (, , );
(() => ("tokenSourceNew was cancelled"));
//Cancel tokenSource2();

In the above example, because tokenSourceNew is associated with tokenSource, tokenSource2, and tokenSource3, as long as one of them is cancelled, tokenSourceNew will also be cancelled, so the printing result of the above example is

tokenSourceNew was cancelled
tokenSource2 was cancelled

Judgment Cancel

The methods we used above are to know that the CancellationTokenSource has been cancelled through callbacks, and it is impossible to know whether the CancellationTokenSource is available through the tag. However, Microsoft has provided us with the IsCancellationRequested attribute to judge. It should be noted that it is the CancellationToken attribute. The specific usage method is as follows

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = ;
//Print is cancelled(() => ("Canceled."));
//Simulate the scene of passing(async ()=> {
    while (!)
    {
        ("It's been executing...");
        await (1000);
    }
});
//Cancel after 5s(5000);

The above code will be cancelled after five seconds, so the token of the CancellationTokenSource will also be cancelled. It is reflected in IsCancellationRequested that the value of true is cancelled, and the false is not cancelled. Therefore, the result of the console output is

Always executing...
Always executing...
Always executing...
Always executing...
Always executing...
Cancelled.

There is another way, which can also actively determine whether the task has been cancelled, but this method is simple and crude, and it directly throws an exception. If you use asynchronous method, you need to pay attention to the way of catching the exceptions inside Task, otherwise the cause of the specific exception may not be sensed externally. Its usage is like this. For the sake of demonstration, I changed to a more direct method.

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = ;
(() => ("Canceled."));
(5000);
while (true)
{
    //If the operation is cancelled, an exception will be directly thrown    ();
    ("It's been executing...");
    await (1000);
}

After five seconds of execution, directly throw : The operation was cancelled.Exception, and pay attention to the exception handling method in asynchronous situations. Through the simple examples above, I believe everyone has a certain understanding of CancellationTokenSource and roughly knows when it can be used, mainly asynchronous cancellation notifications, or limited time operation notifications, etc. CancellationTokenSource is a good artifact, simple to use and powerful functions.

Source code exploration

Through the above example, I believe everyone has a basic understanding of CancellationTokenSource. It is really powerful and very simple to use. This is also the subtlety of C# language. It is very practical and makes you feel very comfortable when using it. You have the urge to kneel down while using it. Let's get to the point, let's take a look at the source code of CancellationTokenSource from a deeper perspective and see what its working mechanism is. The source code posted in this article has been simplified by the blogger. After all, there are too many source codes and it is unlikely that all of them will be pasted. The main purpose is to follow its ideas to understand how it works.

Start with construction

Because this time, there is a relatively important constructor in the initialization function of CancellationTokenSource, which is to set the timeout operation. So let's start with its constructor[Click to view the source code 👈]

//Global statusprivate volatile int _state;
//The status value is not canceledprivate const int NotCanceledState = 1;

/// <summary>
/// Initialization state without parameters/// </summary>
public CancellationTokenSource() => _state = NotCanceledState;

/// <summary>
/// Timely cancel the structure/// </summary>
public CancellationTokenSource(TimeSpan delay)
{
    //Get the milliseconds of the timespan    long totalMilliseconds = (long);
    if (totalMilliseconds < -1 || totalMilliseconds > )
    {
        throw new ArgumentOutOfRangeException(nameof(delay));
    }
    //Call InitializeWithTimer    InitializeWithTimer((int)totalMilliseconds);
}

public CancellationTokenSource(int millisecondsDelay)
{
    if (millisecondsDelay < -1)
    {
        throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
    }
    //Call InitializeWithTimer    InitializeWithTimer(millisecondsDelay);
}

There is nothing to say about the parameterless constructor, it is to initialize the initial value of NotCanceledState for the global state, that is, the initialization state. What we are more concerned with is the constructor that can be canceled regularly. Although they are two constructors, they have the same destination. They are essentially passed millisecond shaping parameters, and the core methods called are InitializeWithTimer. It seems to be a timer operation. This is not surprising. Let's take a look at the implementation of the InitializeWithTimer method.[Click to view the source code 👈]

//Task completion status valueprivate const int NotifyingCompleteState = 2;
//Timerprivate volatile TimerQueueTimer? _timer;
//Timer callback initializationprivate static readonly TimerCallback s_timerCallback = TimerCallback;
//The essence of the timer callback delegate is the NotifyCancellation method of the CancellationTokenSource calledprivate static void TimerCallback(object? state) => 
    ((CancellationTokenSource)state!).NotifyCancellation(throwOnFirstException: false);

private void InitializeWithTimer(uint millisecondsDelay)
{
    if (millisecondsDelay == 0)
    {
        //If the timed milliseconds are 0, set the global state to NotifyingCompleteState        _state = NotifyingCompleteState;
    }
    else
    {
        //If the timeout millisecond is not 0, the timer will be initialized and the timer timing callback will be set        _timer = new TimerQueueTimer(s_timerCallback, this, millisecondsDelay, , flowExecutionContext: false);
    }
}

Through this method, we can clearly see that the core operation of timing initialization is actually to initialize a timer, and the timing time is the number of milliseconds we initialize the passed, where s_timerCallback is a timed callback function, that is, if you wait for the timeout, this delegation will be called. Its essence is the NotifyCancellation method of CancellationTokenSource, which is the operation after the timeout. This method handles the operation after the timeout[Click to view the source code 👈]

//Signal control class, use the signal to determine whether it is necessary to continue execution or blockprivate volatile ManualResetEvent? _kernelEvent;
//throwOnFirstException function indicates whether an exception is thrown if canceledprivate void NotifyCancellation(bool throwOnFirstException)
{
    //If the task has been cancelled, the timer will be directly released    if (!IsCancellationRequested && (ref _state, NotifyingState, NotCanceledState) == NotCanceledState)
    {
        TimerQueueTimer? timer = _timer;
        if (timer != null)
        {
            _timer = null;
            ();
        }
        //The semaphore involves an important property. WaitHandle will say next        _kernelEvent?.Set(); 
        //Execution of cancellation is the core of cancellation. When talking about cancellation, we will focus on this        ExecuteCallbackHandlers(throwOnFirstException);
        (IsCancellationCompleted, "Expected cancellation to have finished");
    }
}

NotifyCancellation handles the timer's time. To put it bluntly, it is the specified time but the operation executed is not manually cancelled. In fact, it is also the cancellation operation executed. This method involves two more important points, which are also the points we will analyze next. Here is an explanation.

  • First is the instance of ManualResetEvent. The function of this class is to control whether to block or perform subsequent operations through a signal mechanism. Another class AutoResetEvent is complemented by this. The effect of these two classes is consistent, except that the ManualResetEvent needs to manually reset the initial state, while the AutoResetEvent will automatically reset. There is no introduction to the two categories here. Students who need to know can use Baidu on their own. And an important property of the CancellationTokenSource class WaitHandle uses it.
  • Another one is the ExecuteCallbackHandlers method, which is the core operation of CancellationTokenSource to perform cancellation operations. In order to ensure the order of reading, we will focus on this method when talking about canceling operations.

As mentioned above, in order to ensure the order of reading and easy to understand, we will explain these two parts in the next article, so we will not initialize the explanation here. Let’s make a mark here in case you continue if you feel that you don’t explain it clearly.

Vignette WaitHandle

Above we mentioned the WaitHandle property of CancellationTokenSource, which is implemented based on ManualResetEvent. This is a slightly independent place, we can explain it first[Click to view the source code 👈]

private volatile ManualResetEvent? _kernelEvent;
internal WaitHandle WaitHandle
{
    get
    {
        ThrowIfDisposed();
        //If initialization is completed, return directly        if (_kernelEvent != null)
        {
            return _kernelEvent;
        }

        //Initialize a ManualResetEvent, given the initial value is false        var mre = new ManualResetEvent(false);
        // If another thread is initialized, release the initialized operation above if another thread is initialized        if ((ref _kernelEvent, mre, null) != null)
        {
            ();
        }

        //If the task has been cancelled, subsequent operations will not be blocked        if (IsCancellationRequested)
        {
            _kernelEvent.Set();
        }
        return _kernelEvent;
    }
}

Through this code, we can see that if the WaitHandle property is used, we can use it to implement a simple blocking notification operation, that is, after receiving the cancel notification operation, we can perform the WaitHandle operation, but WaitHandle is internally modified, how should we use it? Don't panic, we know that the token attribute of CancellationTokenSource obtains the CancellationToken instance[Click to view the source code 👈]

public CancellationToken Token
{
    get
    {
        ThrowIfDisposed();
        return new CancellationToken(this);
    }
}

Directly instantiate a CancellationToken instance and return it, and pass the current CancellationTokenSource instance, find the constructor of CancellationToken[Click to view the source code 👈]

private readonly CancellationTokenSource? _source;
internal CancellationToken(CancellationTokenSource? source) => _source = source;
public WaitHandle WaitHandle => (_source ?? CancellationTokenSource.s_neverCanceledSource).WaitHandle;

Through the above code, we can see that through the CancellationToken instance, we can use the WaitHandle property to achieve the effect of accessing it. Just saying it may be a bit confused. Through a simple example, let's understand how WaitHandle is used. Let's take a look at it briefly.

CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = ;
(() => ("Canceled."));
(5000);
(()=> {
    ("Before blocking");
    ();
    ("The blocking is cancelled, execution is here.");
});
("Execution here");

Before the CancellationTokenSource is cancelled, the() method will block subsequent execution, that is, the following output will not be output for the time being. Only after the CancellationTokenSource executes the Cancel operation, the Set method of ManualResetEvent is called in the Cancel operation and stops blocking, the subsequent output will be executed until this is a synchronous operation. If students who know ManualResetEvent believe that this is not difficult to understand. To demonstrate the effect, I used Task to demonstrate the asynchronous situation, so the execution result is as follows

Execute here
Before blocking
Blocking is cancelled and execution is here.
Cancelled.

Register operation

Above we roughly explained some initialization-related and auxiliary operations. Next, let's take a look at the core registration operation. The purpose of the registration operation is to cancel or time out the CancellationTokenSource. The registration of the Register is not directly performed by the CancellationTokenSource, but operates through its Token attribute, namely the CancellationToken instance. Without further ado, it directly finds the CancellationToken Register method.[Click to view the source code 👈]

public CancellationTokenRegistration Register(Action callback) =>
Register(
    s_actionToActionObjShunt,
    callback ?? throw new ArgumentNullException(nameof(callback)),
    useSynchronizationContext: false,
    useExecutionContext: true);

It directly calls its own overloaded method, pay attention to a few parameters, and if you look at the details, you still need to pay attention to the method parameters. The process is omitted, and the lowest method is directly found[Click to view the source code 👈]

private CancellationTokenRegistration Register(Action<object?> callback, object? state, bool useSynchronizationContext, bool useExecutionContext)
{
    if (callback == null)
        throw new ArgumentNullException(nameof(callback));

    //_source is the CancellationTokenSource passed down    CancellationTokenSource? source = _source;
    //The essence is to call the InternalRegister method of the CancellationTokenSource    return source != null ?
        (callback, state, useSynchronizationContext ?  : null, useExecutionContext ? () : null) :
        default; 

From this lowest method, we can know that its essence is to call the InternalRegister method of CancellationTokenSource. The core operations are not in the CancellationToken or in the CancellationTokenSource class. CancellationToken is more like a performance class that relies on the CancellationTokenSource. Take a look at the InternalRegister method[Click to view the source code 👈]

//Initialize the CallbackPartition arrayprivate volatile CallbackPartition?[]? _callbackPartitions;
//Get the length of the initialization array, and get it based on the current number of CPU coresprivate static readonly int s_numPartitions = GetPartitionCount();

internal CancellationTokenRegistration InternalRegister(
    Action<object?> callback, object? stateForCallback, SynchronizationContext? syncContext, ExecutionContext? executionContext)
{
    //Judge whether it has been cancelled    if (!IsCancellationRequested)
    {
        //If it has been released, return directly        if (_disposed)
        {
            return default;
        }
        CallbackPartition?[]? partitions = _callbackPartitions;
        if (partitions == null)
        {
            //The first call to initialize the CallbackPartition array            partitions = new CallbackPartition[s_numPartitions];
            //Judge if _callbackPartitions is null, then assign partitions to _callbackPartitions            partitions = (ref _callbackPartitions, partitions, null) ?? partitions;
        }
        //Get the partition subscript used by the current thread        int partitionIndex =  & s_numPartitionsMask;
        //Get a CallbackPartition        CallbackPartition? partition = partitions[partitionIndex];
        if (partition == null)
        {
            //Initialize CallbackPartition instance            partition = new CallbackPartition(this);
            //If the partitionIndex subscript position of partitions is null, use partition to fill            partition = (ref partitions[partitionIndex], partition, null) ?? partition;
        }

        long id;
        CallbackNode? node;
        bool lockTaken = false;
        //Lock operation        (ref lockTaken);
        try
        {
            id = ++;
            //Get CallbackNode, where this is really stored callbacks, don't be confused by the List name, actually you need to build a link list            node = ;
            if (node != null)
            {
                //This is more interesting. If CallbackNode is not the first time, then assign the latest value to FreeNodeList                 = ;
            }
            else
            {
                //Initialize a CallbackNode instance for the first time                node = new CallbackNode(partition);
            }
             = id;
            //Register's callback operation is assigned to CallbackNode's Callback             = callback;
             = stateForCallback;
             = executionContext;
             = syncContext;

            //Build a CallbackNode linked list. From the following code, we can see that the built is actually a reverse order linked list. The latest CallbackNode is the header             = ;
            if ( != null)
            {
                 = node;
            }
            //Callbacks records the current node. If a new node comes in next time, it will be the Next node of the new node             = node;
        }
        finally
        {
            //Release the lock            (useMemoryBarrier: false); 
        }
        //Create the CancellationTokenRegistration instance using the CallbackNode node generated by the current registration callback        var ctr = new CancellationTokenRegistration(id, node);
        //If it is not cancelled, return directly        if (!IsCancellationRequested || !(id, node))
        {
            return ctr;
        }
    }
    //Walk here to indicate that IsCancellationRequested has equal to true, that is, it has been cancelled, and the callback is directly executed    callback(stateForCallback);
    return default;
}

Here comes a relatively core class, which is CallbackPartition, which is an internal class. Its main purpose is to assist in building linked list operations that execute callbacks. Its implementation is probably like this.[Click to view the source code 👈]

internal sealed class CallbackPartition
{
    public readonly CancellationTokenSource Source;
    //Spin lock is used    public SpinLock Lock = new SpinLock(enableThreadOwnerTracking: false);
    public CallbackNode? Callbacks;
    public CallbackNode? FreeNodeList;
    public long NextAvailableId = 1; 

    public CallbackPartition(CancellationTokenSource source)
    {
        Source = source;
    }

    internal bool Unregister(long id, CallbackNode node)
    {
        //If there is content here, I won't list it. To determine whether CallbackNode has been cancelled. If it is false, it means that it has not been cancelled.    }
}

I have not listed the content of Unregister for the time being because it is related to cancellation. Let's look at it again when it comes to cancellation. If true returns, it means that the cancellation is successful. The core of this class is to assist in the construction of the Register callback linked list. Its core is to operate the CallbackNode node and the callback linked list it builds. CallbackNode is a node definition of the linked list, and its rough structure is as follows[Click to view the source code 👈]

internal sealed class CallbackNode
{
    public readonly CallbackPartition Partition;
    //The core of building the linked list Prev and Next    public CallbackNode? Prev;
    public CallbackNode? Next;

    public long Id;
    //The callback operation is recorded by this delegate    public Action<object?>? Callback;
    public object? CallbackState;
    public ExecutionContext? ExecutionContext;
    public SynchronizationContext? SynchronizationContext;

    public CallbackNode(CallbackPartition partition)
    {
        Partition = partition;
    }

    public void ExecuteCallback()
    {
        //There is also code here, which will not be listed for the time being. It will be explained separately when canceling it.    }
}

Here, the core operations involved in Register have been listed. Since the source code related is quite confusing, it is actually quite understandable if you follow it, the general implementation idea is still understandable. Here I will summarize its implementation idea roughly.

  • First, we build the CallbackPartition array. The length of the array is determined based on the number of cores of the CPU. Each CallbackPartition is the core of the operation. In order to prevent too many threads from operating a CallbackPartition instance at the same time, it adopts the idea of ​​partitioning for different threads. CallbackPartition maintains the class CallbackNode that builds the linked list node.
  • CallbackNode is the core of the linked list. Each instance of CallbackNode is a node of the linked list. From its own containing Prev and Next attributes, it can be seen that it is a bidirectional linked list.
  • The core function of CallbackPartition is to build the callbacks coming in from the Register. From the operations in the InternalRegister method above, we can know that through the help of CallbackPartition, the CallbackNode node is built into a reverse-order linked list, that is, the latest CallbackNode instance is the first node of the linked list, and the oldest CallbackNode instance is the tail node of the linked list. Every callback that register comes in is wrapped as a CallbackNode and added to this linked list.

In the InternalRegister method above, we see that when operating CallbackNode, SpinLock spin lock is used. SpinLock is faster in the case of short-term locking, because spin locks don't essentially let the thread sleep, but instead loops around to try to access the resource until it is available. Therefore, when the spin lock thread is blocked, the thread context switch is not performed, but idle waits. For multi-core CPUs, the overhead of switching thread contexts is reduced, thereby improving performance.

Cancel operation

We have seen the registration-related operations above. Registration is relatively unified, just one operation method. There are two ways to cancel, one is to cancel the timeout, and the other is to cancel the voluntarily. Next, let’s take a look at how these two ways are operated separately.

Cancel operation

First, let’s look at the operation method of proactive cancellation. This is the simplest and most direct way, and this method belongs to the CancellationTokenSource class. If you don’t say much, just look at the implementation.[Click to view the source code 👈]

public void Cancel() => Cancel(false);

public void Cancel(bool throwOnFirstException)
{
    ThrowIfDisposed();
    NotifyCancellation(throwOnFirstException);
}

The key point is that the Cancel method is actually called NotifyCancellation method. We have already seen this method above. When constructing the CancellationTokenSource in a timed manner, there is an automatic cancellation operation. It is mentioned that the core of the NotifyCancellation method is the ExecuteCallbackHandlers method, which is the core operation of the CancellationTokenSource to perform the cancellation operation. I also said that in order to ensure the order of reading, we will focus on this method when talking about canceling operations. It seems that this moment has finally arrived. Just open the ExecuteCallbackHandlers method[Click to view the source code 👈]

private volatile int _threadIDExecutingCallbacks = -1;
private volatile CallbackPartition?[]? _callbackPartitions;
private const int NotifyingCompleteState = 3;
private void ExecuteCallbackHandlers(bool throwOnFirstException)
{
    //Get the current thread ID    ThreadIDExecutingCallbacks = ;
    //Set _callbackPartitions to null, but partitions are not null, because Exchange returns a change of the previous value    CallbackPartition?[]? partitions = (ref _callbackPartitions, null);
    //If partitions are null, it means that the callback has been notified to complete the status and returns directly    if (partitions == null)
    {
        (ref _state, NotifyingCompleteState);
        return;
    }

    List<Exception>? exceptionList = null;
    try
    {
        //Transtraighten the CallbackPartition array        foreach (CallbackPartition? partition in partitions)
        {
            //The CallbackPartition instance is null, which means that the partition has not been used and skipped directly            if (partition == null)
            {
                continue;
            }

            //Loop processing of CallbackNode link table            while (true)
            {
                CallbackNode? node;
                bool lockTaken = false;
                //Lock the current operation                (ref lockTaken);
                try
                {
                    //The nodes that get the linked list                    node = ;
                    //For null, it means that there is no Register interrupt directly                    if (node == null)
                    {
                        break;
                    }
                    else
                    {
                        //If the linked list traversal is not a tail node, cut off the association with the next node                        if ( != null)  = null;
                        // Assign the next node to Callbacks                         = ;
                    }
                    //The current execution node ID                    _executingCallbackId = ;
                     = 0;
                }
                finally
                {
                    //Exit the lock                    (useMemoryBarrier: false); 
                }

                try
                {
                    //If the synchronization context is passed at that time, the ExecuteCallback delegation will be called directly in the context at that time.                    if ( != null)
                    {
                        (static s =>
                        {
                            var n = (CallbackNode)s!;
                             = ;
                            ();
                        }, node);
                        ThreadIDExecutingCallbacks = ; 
                    }
                    else
                    {
                        //If SynchronizationContext is not passed, the ExecuteCallback delegation will be called directly                        // That is, the registration delegation called Register                        ();
                    }
                }
                catch (Exception ex) when (!throwOnFirstException)
                {
                    (exceptionList ??= new List<Exception>()).Add(ex);
                }
            }
        }
    }
    finally
    {
        // Set the global status to notification completion status        //That is, the Register callback has been called        _state = NotifyingCompleteState;
        (ref _executingCallbackId, 0);
        (); 
    }

    // If there is an exception in the middle, throw it    if (exceptionList != null)
    {
        ( > 0, $"Expected {} > 0");
        throw new AggregateException(exceptionList);
    }
}

The ExecuteCallback method is a method of the CallbackNode class, which is the method that is omitted when we list the CallbackNode class structure above. Its main function is to call the Register callback, that is, to execute the delegate in the Register. I will make up for what I owe. Note that this is the CallbackNode class. Let's take a look at the implementation.[Click to view the source code 👈]

public ExecutionContext? ExecutionContext;
public void ExecuteCallback()
{
    ExecutionContext? context = ExecutionContext;
    //If the ExecutionContext is allowed to be passed during Register, then the callback is executed directly using this context.    //Callback delegation means the delegate operation that bears the Register    if (context != null)
    {
        (context, static s =>
        {
            (s is CallbackNode, $"Expected {typeof(CallbackNode)}, got {s}");
            CallbackNode n = (CallbackNode)s;

            ( != null);
            ();
        }, this);
    }
    else
    {
        (Callback != null);
        //Callback directly in the current thread        //Callback delegation means the delegate operation that bears the Register        Callback(CallbackState);
    }
}

We have listed the important operations of ExecuteCallbackHandlers, the core method of cancellation. In fact, when we see the registration idea, we can already guess the general idea of ​​executing cancelled callbacks. Since the zipper was performed during the Register, canceling the registration callback must be the Callback in the variable link execution. Let's summarize it roughly.

  • After executing Cancel, the core operation is still traversing the built CallbackNode list. We have said before that the built CallbackNode list is a reverse order list, and the latest node is placed at the beginning of the linked list. This explains why when we have multiple delegates in the above example Register, the first output is the last registration delegate.
  • Register registration has parameters to determine whether the current synchronization context SynchronizationContext and execution context ExecutionContext need to be passed. The function is to whether the Callback callback operation is performed in the context environment at that time.
  • In the above traversal code, we saw that the operation =null will be executed, in order to disconnect the relationship between the current linked list node and the upper and lower nodes. I personally feel that it is to cut off object references for easy release and prevent memory leakage. It also shows that by default, the execution of Register's callback function is one-time. After the Cancel operation is executed, the current CancellationToken instance will be invalid.

CancelAfter Operation

When we demonstrated before, we said that there are two ways to perform timeout cancellation operations. One is to pass the timeout time when building the CancellationTokenSource instance construction, and the other is to use the CancelAfter operation. This method means canceling after the specified time. The effect is equivalent to the operation of passing the timeout time when instantiating the CancellationTokenSource. If you don't say much nonsense, just list the code.[Click to view the source code 👈]

public void CancelAfter(TimeSpan delay)
{
    long totalMilliseconds = (long);
    if (totalMilliseconds < -1 || totalMilliseconds > )
    {
        throw new ArgumentOutOfRangeException(nameof(delay));
    }
    //The overloaded CancelAfter method is called    CancelAfter((int)totalMilliseconds);
}

private static readonly TimerCallback s_timerCallback = obj =>
{
    ((CancellationTokenSource)obj).NotifyCancellation(throwOnFirstException: false); 
};

public void CancelAfter(int millisecondsDelay)
{
    //The number of milliseconds passed cannot be less than -1    if (millisecondsDelay < -1)
    {
        throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));
    }

    //If it has been cancelled, return directly    if (IsCancellationRequested)
    {
        return;
    }

    //Register a timer to execute s_timerCallback    //s_timerCallback has been introduced above. This is the NotifyCancellation method called CancellationTokenSource    TimerQueueTimer? timer = _timer;
    if (timer == null)
    {
        timer = new TimerQueueTimer(s_timerCallback, this, , , flowExecutionContext: false);
        TimerQueueTimer? currentTimer = (ref _timer, timer, null);
        if (currentTimer != null)
        {
            ();
            timer = currentTimer;
        }
    }

    try
    {
        ((uint)millisecondsDelay, );
    }
    catch (ObjectDisposedException)
    {
    }
}

Through the above source code, we can see that the operation code of CancelAfter and the code of the passing timeout constructor of CancellationTokenSource are basically the same. Both timerQueueTimer trigger the call to CancellationTokenSource's NotifyCancellation method. The core implementation of the NotifyCancellation method is the ExecuteCallbackHandlers method. We have explained these methods above, so we will not repeat them. In this way, we will complete the cancellation of related operations.

Summarize

In this article, we mainly explain the C# cancellation token CancellationTokenSource. Although there are not many classes designed, there are not many source codes in this part, and it only explains some of the source codes for core functions. Interested students can read the relevant code of this class by themselves. If you think your GitHub is not good, recommend a website that can read the source code of CoreCLR. This website sees the latest source code of CoreCLR. It is very convenient to connect directly to GitHub, but there are some differences between the source code of the latest version and the stable version, so you still need to pay attention to this. Because the article is long and the author has limited technical and writing skills, here is a simple summary

  • The purpose of CancellationTokenSource is to perceive cancellation operations. The Register callback, WaitHandle, and IsCancellationRequested involved can all implement this function. Of course, it also supports timeout cancellation operations.
  • CancellationTokenSource's Register and Cancel are paired. Although there are CancelAfter and constructs the way to pass timeout, their essence is the same as Cancel operation.
  • The core operation principle of CancellationTokenSource is to build reverse-order linked lists through CallbackPartition and CallbackNode. When registering, it is used to build a linked list through Callback. When Cancel, it is traversed to execute Callback. Although there are a lot of extra operations, the core working method is to operate the linked list.
  • It should be noted that by default, the CancellationToken generated by the CancellationTokenSource is one-time, and there is no way to reset it after cancellation. Of course, Microsoft has provided us with IChangeToken to solve the problem of repeated triggering of CancellationToken, please feel free to use it.

Because this article is long and my ability is limited, my writing style is even more average. If you don’t explain clearly, I hope you can understand it, or if you are interested, you can read the source code by yourself. When it comes to reading source code, everyone has their own concerns. My general intention is to understand its principles and learn its code style or ideas. Learning is endless, and the result is sometimes not that important, but the process is important. Just like many people pursuing what heights they can reach, success is actually just a manifestation of growth, just like if you are not satisfied with the status quo, it means that you have never thought of changing yourself a long time ago.

This is the article about using examples to briefly explain the C# CancellationTokenSource. For more related C# CancellationTokenSource content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!