Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

CancellationToken vs CancellationChangeToken

What are the differences between CancellationToken and CancellationChangeToken? When do I use which one?

It seems that they can be used for the same purpose. What am I missing?

like image 753
satellite satellite Avatar asked May 13 '20 14:05

satellite satellite


1 Answers

Short answer:

CancellationChangeToken is a thin wrapper around CancellationToken, which exposes the token's cancellation state through the HasChanged property.

Since CancellationChangeToken implements the IChangeToken interface, it can be passed to any object that reacts to token changes, for example an IMemoryCache: Each cache entry can be set to expire under a specific set of conditions, which are abstracted by IChangeToken instances. These conditions may be, for example, configuration changes, or explicit invalidation requests by the user.

The CancellationChangeToken wrapper can be passed to one or more cache entries, in order to evict them upon cancellation. The devs could also have decided to directly take a CancellationToken; this would however have complicated the internal logic and the external API, compared to the abstraction through IChangeToken, which is very clean, extensible and working just as well.


Long answer:

Interface IChangeToken

IChangeToken defines a means to track some kind of token, check whether it has changed (HasChanged), and possibly automatically trigger some callbacks once it has changed.

There are various implementations for IChangeToken; for example, they track files or configuration options.

What CancellationChangeToken does

A look at the source code quickly shows that the CancellationChangeToken is a thin IChangeToken wrapper around CancellationToken:

public bool HasChanged => Token.IsCancellationRequested;

So its "changed" state directly corresponds to the underlying token's cancellation state.

On first sight, this seems quite odd: A CancellationToken already offers support for registering automatic callbacks, so why introduce a wrapper that exposes much less functionality?

Well, there is actually at least one use case where an abstracted way of observing token states is needed:

Caching

ASP.NET Core offers different means of caching; I will use IMemoryCache as an example here, which offers a simple built-in memory-based key/value cache, and is usable with minimal effort.

To reduce performance impact for each request, it makes sense to cache frequently needed, expensive computations. However, one needs a way to keep track of active cache entries in order to evict them as soon as they become outdated. The problem is, that the cache entries can have very different conditions for becoming outdated, e.g. they depend on configuration values or local files (and need to be updated as soon as these configuration values or local files change), or have a regular time out.

The IChangeToken interface offers a simple way to abstract these eviction conditions away, such that the cache only has to check the state of the given IChangeToken object, instead of directly watching files, timers, and so on. The cache might even register a callback, such that a change of the token state directly triggers the eviction logic, so no polling is needed.

Back to our CancellationChangeToken

A CancellationToken allows to set a delay until it is automatically cancelled, so after wrapping it with a CancellationChangeToken, we can pass it to a cache entry for automated eviction:

public IActionResult Index()
{
    if(!_memoryCache.TryGetValue("entry1", out DateTime cachedValue))
    {
        cachedValue = DateTime.Now;

        var cacheOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(5000).Token));

        _memoryCache.Set("entry1", cachedValue, cacheOptions);
    }

    return View("Index", cachedValue);
}

In this example, we cache the current timestamp. After 5 seconds, the cancellation token is cancelled, and the timestamp gets evicted.

However, this is does not yet justify the use of a CancellationToken, since the IMemoryCache already offers a way to set an expiration time through MemoryCacheEntryOptions.

Now we get to the real strengths of the token-based eviction: We can

  • combine multiple IChangeToken instances into one CompositeChangeToken, and thus evict a cache entry once any of these instances change.
  • use the same token for multiple cache entries, thus evicting them all at once.

The latter case is particularly interesting for us: Some cache entries might depend on each other, so when one becomes invalid, we might want to evict all of them. This can be achieved by assigning a shared CancellationToken to their values, and a corresponding CancellationChangeToken to the entries themselves. Once one of the cached values notices that it has become outdated (e.g. by being polled by some other part of the application), it cancels the token. The cancellation propagates to all cache entries that track the corresponding change token, and thus invalidates them all at once.

like image 114
janw Avatar answered Sep 29 '22 00:09

janw