CancellationTokenSource and CancellationToken
CancellationTokenSource
CancellationTokenSource is a class used to create and control CancellationTokens. It is the key point in triggering a cancel operation and provides methods to trigger a cancel operation (such as Cancel) and attributes to check whether a cancellation has been requested (such as IsCancellationRequested).
Main methods:
- Cancel(): Triggers the cancel operation.
- Dispose(): Release the resources occupied by the CancellationTokenSource.
Main attributes:
- IsCancellationRequested: Indicates whether cancellation has been requested.
- Token: Get the CancellationToken associated with this CancellationTokenSource.
CancellationToken
CancellationToken is a lightweight object used to pass cancel signals between threads. It does not perform any cancellation operation itself, but rather serves as a flag to cancel the request. When an operation should be cancelled, the CancellationTokenSource associated with the CancellationToken will send a cancel signal, and then any code listening to the CancellationToken can respond to the cancel request.
Main methods:
- Register(Action callback): Register a callback that is called when the cancel operation is triggered.
- ThrowIfCancellationRequested(): If cancellation is requested, an OperationCanceledException exception is thrown.
Main attributes:
- IsCancellationRequested: Indicates whether cancellation has been requested.
Example of usage:
using System; using ; using ; class Program { static async Task Main() { var cts = new CancellationTokenSource(); var token = ; var task = (() => DoWork(token), token); // Cancel the operation after simulation for a period of time (2000); (); try { await task; // If the operation is cancelled, an exception will be thrown here } catch (OperationCanceledException) { ("Operation was canceled."); } } static void DoWork(CancellationToken token) { for (int i = 0; i < 10; i++) { if () { ("Cancellation requested."); return; } (500); // Simulation work ($"Working... {i}"); } } }
When using CancellationToken and , there are several key points to pay attention to:
CancellationToken passed to: This CancellationToken is used to control the life cycle of the task created. If this token is cancelled (i.e., call()), then Task will receive a cancel request, but this does not mean that the task will stop executing immediately. The code in the task needs to explicitly check the cancel request (usually by calling () or checking the properties) and decide whether to stop execution based on this.
CancellationToken passed to the DoWork method: This CancellationToken is passed as a parameter to the DoWork method. Inside the DoWork method, you can decide whether to continue execution based on the status of this token. If a cancel request is detected (IsCancellationRequested is true), you can exit the method in advance or perform a cleanup operation. However, setting the cancel state of this token will not automatically stop the execution of the DoWork method; you need to write corresponding logic to handle the cancel request.
Cancel of task: Even if you cancel the CancellationToken, the Task object itself will not immediately change to the "Cancelled" state. Instead, it becomes "Canceled Request" state, meaning that the task has been requested to be cancelled, but the task may still be in execution. The task stops execution and eventually becomes a "Canceled" state only when the code in the task explicitly checks and responds to a cancelled request.
Exception handling: If a cancel request is detected in the task and () is called, an OperationCanceledException will be thrown. This exception is often considered part of a normal cancel process, rather than an error that needs to be caught and processed.
Therefore, in summary, when the CancellationToken is cancelled, the created task will receive a cancel request, but the DoWork method itself will not automatically stop execution. You need to write logic inside the DoWork method to respond to this cancel request and decide whether to stop execution based on it. Likewise, the task itself does not stop execution immediately; it will continue to execute until a code that checks for cancel request is encountered and decides whether to stop based on the logic of that code.
What has CancellationToken been canceled
In C#, when the DoWork method passes to check the property and returns ahead of time, it simply exits from the method. This does not directly end or cancel the underlying Task object. However, since the created Task object is associated with the execution of the DoWork method, when the DoWork method returns, the Task object sets its status to "RanToCompletion" instead of "Canceled".
However, if you want to make the state of the Task object "Canceled" when the cancel request is detected, you need to throw an OperationCanceledException. This is usually done by calling the() method, which throws an exception when the cancel request has been sent.
Here is an example of a modified DoWork method that throws an OperationCanceledException when a cancel request is detected:
static void DoWork(CancellationToken token) { for (int i = 0; i &lt; 10; i++) { (); // If the cancel request has been sent, an exception is thrown ($"DoWork: Working {i + 1}..."); (500); // Simulation time-consuming operation } ("DoWork: Work completed."); // This line of code will not actually be executed because the loop will throw an exception at a certain point}
In this example, if the CancellationToken cancel request is sent (i.e. the() method is called), the ThrowIfCancellationRequested() method will throw an OperationCanceledException exception. This exception bubbles into the called code and eventually causes the Task object's state to become "Canceled".
However, note that even if you throw an OperationCanceledException, any local resources in the Task object (such as file handles, database connections, etc.) still need to be explicitly released by your code. The exception just changes the state of the Task and notifies the caller that the task has been cancelled, but it does not automatically perform any cleanup work.
ManualResetEvent
ManualResetEvent is a synchronous primitive that allows threads to communicate through signals. When the event is in an unissued state, the thread blocks when calling methods such as WaitOne, WaitAny, or WaitAll, until other threads mark the event as issued by calling the Set method. The event remains issued until the Reset method is called to reset it to the unissued state.
Main methods:
- Set(): Set ManualResetEvent to the issued state, allowing one or more waiting threads to continue execution.
- Reset(): Reset ManualResetEvent to the unissued state, blocking the waiting thread.
- WaitOne(): Blocks the current thread until ManualResetEvent is set to the emitted state.
Example of usage:
Several Tasks are created, all waiting for the same ManualResetEvent to be set. Once the event is set, all waiting tasks will continue to be executed.
using System; using ; using ; class Program { static ManualResetEvent resetEvent = new ManualResetEvent(false); static async Task Main(string[] args) { // Create and start multiple tasks Task[] tasks = new Task[3]; for (int i = 0; i < ; i++) { int taskId = i; // Capture loop variables tasks[i] = (() => Worker(taskId)); } // Simulate some work... ("Main thread is doing some work..."); await (2000); // Asynchronous waiting // Set events to allow all waiting tasks to continue execution ("Setting the event to allow all tasks to continue..."); (); // Wait for all tasks to complete await (tasks); ("All tasks have completed."); } static void Worker(int taskId) { ($"Task {taskId} is waiting..."); (); // Wait for event to be set ($"Task {taskId} continues..."); // Simulate some work... (1000); // Note: Await() is usually used in async methods } }
The Worker method above is actually synchronous and it is used to simulate work, which is usually not the best practice in async/await mode. In the async method, you should use await() to wait asynchronously. However, since the Worker method is designed to work with ManualResetEvent and is called in, it is acceptable to use here.
If you want an example that is entirely based on async/await and does not use ManualResetEvent (because async/await provides a more natural asynchronous wait mode), you can do this:
using System; using ; class Program { static Task completionSignal = new TaskCompletionSource<bool>().Task; static async Task Main(string[] args) { // Create and start multiple tasks Task[] tasks = new Task[3]; for (int i = 0; i < ; i++) { tasks[i] = (async () => await Worker(i)); } // Simulate some work... ("Main thread is doing some work..."); await (2000); // Set events to allow all waiting tasks to continue execution ((TaskCompletionSource<bool>)).SetResult(true); // Wait for all tasks to complete await (tasks); ("All tasks have completed."); } static async Task Worker(int taskId) { ($"Task {taskId} is waiting..."); await completionSignal; // Wait for event to be set ($"Task {taskId} continues..."); // Simulate some asynchronous work... await (1000); } }
In this async/await example, we use TaskCompletionSource<T> to create a task that can be waited for and mark it completed by calling SetResult when appropriate. This is a more natural way to synchronize multiple asynchronous operations in the async/await world.
For infinite looping threads and scenes where external control is required to pause/recover, use ManualResetEvent you can set and reset it multiple times as needed.
using System; using ; using ; class Program { static ManualResetEvent pauseEvent = new ManualResetEvent(true); // Initialize to set state, that is, the thread does not pause static void Main(string[] args) { // Start the worker thread (WorkerThread); //Console input to control pause and recovery ("Press 'p' to pause, 'r' to resume, or 'q' to quit."); while (true) { var key = (true).KeyChar; switch (key) { case 'p': (); // Pause thread ("Worker thread paused."); break; case 'r': (); // Recover thread ("Worker thread resumed."); break; case 'q': // Here is a way to notify the worker thread to exit gracefully, but in this simple example we exit the program directly (0); break; } } } static void WorkerThread() { while (true) { // Wait for a pause event (); // Execute work... ("Worker thread is working..."); // Simulation work takes time (500); // Note: In real scenarios, you may not need to call WaitOne in every loop iteration, // Unless you really want to check the pause status for every iteration. // In this example, we do this just for demonstration. } } }
There is a problem with the above code example: it checks for pause status in every loop iteration, which can lead to unnecessary performance overhead. In practice, you might want to check the pause state only when needed, or break the work into larger blocks and check the pause state after each block.
AutoResetEvent
Both AutoResetEvent and ManualResetEvent are used for synchronization and communication between threads, but there are some key differences between them:
- Reset behavior: AutoResetEvent will automatically reset to the unset state after releasing a waiting thread, while ManualResetEvent needs to explicitly call the Reset() method to reset to the unset state.
- Signal notification: When AutoResetEvent calls the Set() method, only one waiting thread is allowed to be awakened; and after the ManualResetEvent calls the Set() method, it will wake up all waiting threads.
The main feature of AutoResetEvent and ManualResetEvent (actually the same thing) is their automatic reset behavior, which makes it very useful in controlling the order of thread execution and synchronizing thread operations, and other method calls are consistent with ManualResetEvent.
This is the end of this article about the implementation of C# Task cancellation. For more related content about C# Task cancellation, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!