Cancellation Token: Handling Infinite Loops with style
Mary 🇪🇺 🇷🇴 🇫🇷
Posted on September 9, 2024
Have you ever written a loop in your code that seems like it will never end? This is a common scenario in software designed to run continuously, such as background services. For example, a web service typically listens on a specific address and port, indefinitely waiting for incoming connections to handle its tasks.
Since these connections can arrive at any time, the service must always be ready, often implying the use of an infinite loop.
Here’s a basic example in C#:
while (true)
{
if (Listener.Pending())
{
ISocketWrapper client = new SocketWrapper(Listener.AcceptSocket());
await AcceptConnectionsAsync(client);
}
}
In this loop, the service checks if a new connection is pending. If it is, it accepts the connection and processes the client. Otherwise, the loop continues, endlessly repeating the same check.
At first glance, an infinite loop seems like a reasonable solution — after all, the service must keep running. And if you need to stop the service, you can simply kill the process. While this might not always cause issues, it's neither the most elegant nor the safest solution.
The Problem with Infinite Loops
Even though an infinite loop may seem fine for now, what happens when the service needs to evolve? For example, what if you need to update the listening address or port? Restarting the entire service might work, but it's often inconvenient and could interrupt ongoing operations. Worse, forcefully stopping a service can lead to data loss or other unintended consequences, especially if the service is in the middle of processing a client request.
This is where a better solution comes into play: we need a way to exit the loop gracefully, without abruptly terminating the process, and that can be called from anywhere in the code. This is where the Cancellation Token comes in.
What is a Cancellation Token?
A Cancellation Token is an object in C# that signals a request to stop a task. By using it in your code, you can implement a controlled exit condition for your infinite loops, ensuring that when a stop request occurs, the service has a chance to properly finish its current work.
Let’s improve the previous example by integrating a CancellationToken
:
public async Task StartListeningAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
if (Listener.Pending())
{
ISocketWrapper client = new SocketWrapper(Listener.AcceptSocket());
await AcceptConnectionsAsync(client);
}
}
// Cleanup work
}
Now, instead of looping indefinitely, the loop continues as long as the CancellationToken
does not signal a cancellation request. This token allows the service to shut down gracefully when it needs to stop.
One Cancellation Token is Good, Two is Better...
Indeed, a complex application may have multiple tasks that are related or independent of each other. When a parent task needs to be canceled, you also need the ability to cancel its child tasks. Otherwise, you may be forced to wait for the child task to finish, which could take a long time.
Several strategies exist to handle this. The simplest is to pass your Cancellation Token to the various subtasks. For example, a slight modification to the AcceptConnectionsAsync
signature would work:
await AcceptConnectionsAsync(client, token);
If we are certain that all child tasks will be canceled with the parent task, this model works well. However, it doesn't allow for the cancellation of a child task without canceling the parent task.
A List or a Graph of Tokens
A good way to address this issue is to have a Cancellation Token for each task or module, providing more control and granularity in stopping tasks. Each token can be stored in a data structure such as a list or graph, depending on your needs. The cancellation of a module and its token could then propagate to the associated subtasks.
There are several simple ways to implement this behavior. For instance, you could use a dictionary where the key is the name of the task or module, and the value is the associated token. This allows you to manage and cancel tasks in an organized manner. Here’s an example implementation in C#:
public class WebServer
{
public Dictionary<string, CancellationTokenSource> CancellationTokens = new Dictionary<string, CancellationTokenSource>();
public readonly CancellationTokenSource GlobalTokenSource = new CancellationTokenSource();
public async Task StopAsync(bool isRestart = false)
{
foreach (var tokenName in CancellationTokens.Keys)
{
Console.WriteLine($"Cancelling token {tokenName}: {CancellationTokens[tokenName].Token}");
CancellationTokens[tokenName].Cancel();
}
// Wait if needed for tasks to finish
await Task.Delay(1000); // Simulate wait
}
}
In this example, the WebServer
class uses a dictionary to store the Cancellation Tokens associated with different tasks or modules. The StopAsync
method iterates through the dictionary keys, canceling each token. This approach ensures that all tasks are properly stopped while providing the flexibility needed to manage tasks individually or in groups.
Conclusion
Wanting to implement an infinite loop in your code should now be a red flag. Before proceeding, it’s essential to ask yourself if a Cancellation Token might be a more appropriate solution to handle the loop more elegantly. By incorporating these tokens, you ensure that your services can be stopped gracefully, without compromising their stability or abruptly interrupting ongoing tasks.
Thank you for taking the time to read this article! I hope it helped you better understand how to efficiently manage infinite loops and Cancellation Tokens in C#. Feel free to leave a comment if you have any questions or ideas to share.
If you want to learn more about C#, come join me on Twitch! I regularly share live programming sessions where we discuss these topics.
Posted on September 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.