pondělí 10. června 2019

Cancellation in .NET

.NET 4.0 introduced support for cancelling asynchronous operations using CancellationToken. It is a cooperative pattern, where one operation has a means to trigger cancellation of another operation by sending a cancellation request to it. In order for this to work, the receiver must listen for such requests and react appropriately. In other words, an operation can only be cancelled when the code is written with cancellation support in mind.

This article will explain how to properly implement this pattern.

CancellationTokenSource

There are the following two sides of  cancellation:

CancellationTokenSource
Allows the sender to trigger cancellation.

CancellationToken
Allows the receiver to receive cancellation request.

In general the approach works as follows:

// the sender:
{
  // the sender operation creates a source ...
  var cts = new CancellationTokenSource();

  // ... and links it to a token
  var token = cts.Token;

  // the token is pased to the receiver
  var task = LongRunningOperation(token);

  ...

  // at some later point in time
  // the source is used to issue a cancellation request
  cts.Cancel();
}
// the receiver:
async Task LongRunningOperation(
  CancellationToken token = default(CancellationToken))
{
  while (...)
  {
    // the receiver periodically checks for cancellation request
    // when triggered, the operation is cancelled
    // by throwing OperationCanceledException
    token.ThrowIfCancellationRequested();

    // do work
    ...
  }
}

An Example

Let me now illustrate this concept with an example.  We will use a simple Windows Forms application, because it allows a straightforward demonstration of the concept, although the approach is universally applicable and can be used with any asynchronous code.

Our application allows to calculate highest prime number lower than a given max value. It contains an input field and two buttons - the Calculate button to start the calculation and the Cancel button to cancel it.


Efficient algorithms exist for this problem, however I have chosen a naive quadratic iteration so that the cancellation code stands out.

The application contains a single Form class:

public partial class Form1 : Form
{
  CancellationTokenSource cts;
  ...
There is a CancellationTokenSource stored as a class field. This is needed so that we can share it between the two button clicked handlers (see below).

  ...
  private long FindLargestPrimeNumber(
    long max, CancellationToken token = default(CancellationToken))
  {
    var result = 1;
    for (int i = 2; i <= max; i++)
    {
      token.ThrowIfCancellationRequested();
      if (IsPrime(i, token))
      {
        result = i;
      }
    }
    return result;
  }

  private bool IsPrime(
    int num, CancellationToken token = default(CancellationToken))
  {
    for (int i = 2; i < num; i++)
    {
      token.ThrowIfCancellationRequested();
      if (num % i == 0)
      {
        return false;
      }
    }

    return true;
  }
  ...
These two methods implement the computation algorithm. Note how the token is used for periodic cancellation checks.

Also note how the token is propagated through the whole call chain. This is a common practice as in general code which doesn't accept a cancellation token is impossible to cancel.

  ...
  private void calculateBtn_Click(object sender, EventArgs e)
  {
    cts = new CancellationTokenSource();
    var token = cts.Token;

    Task.Run(() =>
    {
      long max = -1;
      this.Invoke((Action)(() => 
      {
        largestPrimeTxt.Clear();
        calculateBtn.Enabled = false;
        cancelBtn.Enabled = true;
        calculatingLbl.Visible = true;
        max = long.Parse(this.maxValueTxt.Text);
      }));

      long prime = -1;
      try
      {
        prime = FindLargestPrimeNumber(max, token);
      }
      catch (OperationCanceledException)
      { }
      
      this.Invoke((Action) (() =>
      {
        calculateBtn.Enabled = true;
        cancelBtn.Enabled = false;
        calculatingLbl.Visible = false;

        if (prime != -1)
        {
          largestPrimeTxt.Text = prime.ToString();
        }
      }));

      cts.Dispose();
    }, token);
  }
  ...
This is the handler to start the computation. Note how a new CancellationTokenSource is created and CancellationToken retrieved. The token is then accessed in the asynchronously executed lambda function.

As explained above, upon cancellation an OperationCanceledException gets thrown deep within the computation algorithm, which then propagates all the way up the call stack. Note the catch block which interrupts the propagation and allows us to make sure the UI gets properly updated.

Since CancellationTokenSource is IDisposable, the Dispose call on the bottom is necessary so that allocated OS resources get freed up.

  ...
  private void cancelBtn_Click(object sender, EventArgs e)
  {
    cts.Cancel();
  }
}
The last piece is the handler to cancel the computation.

Linked Tokens and Timeouts

In addition to cancellations triggered by user actions, it is also possible to set up automatic cancellations based on timeouts. Since .NET 4.5 there is a convenient way to express this.

The timeout can be passed into the CancellationTokenSource constructor:

// create a token which will automatically get cancelled
// after a given time
var timespan = ...;
var cts = new CancellationTokenSource(timespan);
var token = cts.Token;
For existing sources, a timeout can be set through the CancelAfter method:

// set up cancellation for an existing token after a given time
var cts = new CancellationTokenSource();
var token = cts.Token;
var timespan = ...;
cts.CancelAfter(timespan);
It is also possible to combine multiple sources of cancellation together. .NET allows to link a CancellationTokenSource to one or multiple existing cancellation tokens. The resulting token is cancelled when any of the linked sources is cancelled.

var token = ...; // original cancellation token
using (var cts = 
  CancellationTokenSource.CreateLinkedTokenSource(token))
{
  var combinedToken = cts.Token;
  ...
}

An Example

We can use these features to modify the prime number calculation to get automatically cancelled when it isn't finished in 30 seconds. 

In this case it's only needed to specify the timeout in CancellationTokenSource constructor:

cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

Sample Code

You can download the full sample code from prime number calculator at GitHub.

Conclusion

In this article I have shown you how to properly implement cancellation in .NET, including timeouts and linked tokens.

0 komentářů:

Okomentovat