CancellationToken 与 CancellationChangeToken

CancellationToken vs CancellationChangeToken

CancellationTokenCancellationChangeToken有什么区别?我什么时候使用哪一个?

看来它们可以用于相同的目的。我错过了什么?

CancellationChangeToken 实现了 IChangeToken 并且它的构造函数接受一个 CancellationToken 有一个参数。

我想说的是,它提供了能够使用期望 IChangeToken 的方法以及期望 CancellationToken 的方法,同时使用相同的标记。

这是来自 Microsoft documentation

的一个(有点人为的)例子
var firstCancellationTokenSource = new CancellationTokenSource();
var secondCancellationTokenSource = new CancellationTokenSource();

var firstCancellationToken = firstCancellationTokenSource.Token;
var secondCancellationToken = secondCancellationTokenSource.Token;

var firstCancellationChangeToken = new CancellationChangeToken(firstCancellationToken);
var secondCancellationChangeToken = new CancellationChangeToken(secondCancellationToken);

var compositeChangeToken = 
    new CompositeChangeToken(
        new List<IChangeToken> 
        {
            firstCancellationChangeToken, 
            secondCancellationChangeToken
        });

这里,一个CompositeChangeToken,构造函数接受一个IChangeToken的列表,用两个CancellationToken实例化。如果没有 CancellationChangeToken class.

那是不可能的

简答:

CancellationChangeTokenCancellationToken 的薄包装,它通过 HasChanged 属性.

公开令牌的取消状态

由于CancellationChangeToken实现了IChangeToken接口,它可以传递给任何对令牌变化做出反应的对象,例如IMemoryCache:每个缓存条目都可以设置为过期在一组特定条件下,这些条件由 IChangeToken 个实例抽象。例如,这些条件可能是配置更改或用户的明确无效请求。

CancellationChangeToken 包装器可以传递给一个或多个缓存条目,以便在取消时逐出它们。开发者也可以决定直接使用 CancellationToken;然而,与通过 IChangeToken 进行的抽象相比,这会使内部逻辑和外部 API 变得复杂,IChangeToken 非常干净,可扩展且工作正常。


长答案:

接口IChangeToken

IChangeToken定义了一种追踪某种token的方法,检查它是否有改变HasChanged), 并可能在 更改.

后自动触发一些 回调

IChangeToken有多种实现方式;例如,他们跟踪文件或配置选项。

CancellationChangeToken 的作用

A look at the source code 快速显示 CancellationChangeTokenCancellationToken:

的薄 IChangeToken 包装器
public bool HasChanged => Token.IsCancellationRequested;

所以它的"changed"状态直接对应底层代币的取消状态。

乍一看,这似乎很奇怪:CancellationToken 已经 offers support for registering automatic callbacks,那么为什么要引入一个公开功能少得多的包装器?

好吧,实际上至少有一个用例需要一种抽象的方式来观察代币状态:

缓存

ASP.NET 核心提供不同的缓存方式;我将在这里使用 IMemoryCache 作为示例,它提供了一个简单的基于内存的内置 key/value 缓存,并且可以轻松使用。

为了减少对每个请求的性能影响,缓存经常需要的、昂贵的计算是有意义的。但是,需要一种方法来跟踪活动的缓存条目,以便在它们过时时立即将其驱逐。问题是,缓存条目可能有非常不同的过时条件,例如它们依赖于配置值或本地文件(并且需要在这些配置值或本地文件更改后立即更新),或者有规律的超时。

IChangeToken 接口提供了一种简单的方法来抽象这些驱逐条件,这样缓存只需要检查给定 IChangeToken 对象的状态,而不是直接查看文件、计时器, 等等。缓存甚至可能会注册一个回调,这样令牌状态的改变会直接触发驱逐逻辑,因此不需要轮询。

回到我们的CancellationChangeToken

一个CancellationToken允许设置一个延迟,直到它被自动取消,所以在用CancellationChangeToken包装它之后,我们可以将它传递给一个缓存条目以进行自动驱逐:

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);
}

在这个例子中,我们缓存当前时间戳。 5 秒后,取消令牌被取消,时间戳被逐出。

但是,这还不能证明使用 CancellationToken 是合理的,因为 IMemoryCache 已经提供了一种通过 MemoryCacheEntryOptions.[=50= 设置过期时间的方法]

现在我们了解基于令牌的驱逐的真正优势:我们可以

  • 将多个 IChangeToken 实例合并为一个 CompositeChangeToken,从而在这些实例中的任何一个发生变化时逐出缓存条目。
  • 对多个缓存条目使用相同的令牌,从而一次性将它们逐出。

后一种情况对我们来说特别有趣:一些缓存条目可能相互依赖,所以当一个变得无效时,我们可能想要驱逐所有它们。这可以通过将共享 CancellationToken 分配给它们的 并将相应的 CancellationChangeToken 分配给 条目 本身来实现。一旦缓存的 values 之一注意到它已经过时(例如,被应用程序的其他部分轮询),它就会取消令牌。取消传播到所有跟踪相应更改令牌的缓存条目,从而一次性使它们全部失效。

CancellationToken 被设计为 readonly struct (我假设这是由于性能原因)所以它不能扩展(继承)。所以如果你想对取消标记使用抽象,你不能使用CancellationToken struct。为此,您必须使用 IChangeToken.

所以到了classCancellationChangeToken。它是一个 class(不是 struct as CancellationToken) 并且它实现了 IChangeToken interface;它包装了一个 CancellationToken struct (真正的取消标记)

所以,如果我们想使用取消令牌的抽象,我们必须使用 IChangeToken 及其实现 ej: CancellationChangeToken.

何时使用 CancellationTokenCancellationChangeToken

如果我们只关心更改通知,我们可以使用 CancellationToken。 但是,如果我们想使用取消令牌作为抽象,我们必须使用 IChangeToken 及其实现(CancellationChangeToken,等等)