如何创建分层的 CancellationTokenSource?

How to Create a Hierarchical CancellationTokenSource?

在我们的项目中,我们决定在CancellationToken的帮助下为用户提供取消机制。

由于项目中的作品结构,我需要一个分层取消机制。通过分层,我的意思是父源取消会导致所有子源被递归取消,但子源取消不会传播到父源。

.NET 中是否有开箱即用的此类选项?如果不是,我不确定向父代币注册一个委托是否足够,或者应该给予进一步的考虑。

是的,此功能开箱即用。查看 CancellationTokenSource.CreateLinkedTokenSource 方法。

Creates a CancellationTokenSource that will be in the canceled state when any of the source tokens are in the canceled state.

示例:

using var parentCts = new CancellationTokenSource();
using var childrenCts = CancellationTokenSource
    .CreateLinkedTokenSource(parentCts.Token);

parentCts.Cancel(); // Cancel the children too
childrenCts.Cancel(); // Doesn't affect the parent

基于the source codeLinked2CancellationTokenSource的实现,我得出了这个实现:

public class HierarchicalCancellationTokenSource : CancellationTokenSource
{
    private readonly CancellationTokenRegistration _parentReg;

    public HierarchicalCancellationTokenSource(CancellationToken parentToken)
    {
        this._parentReg = parentToken.Register(
            static s => ((CancellationTokenSource)s).Cancel(false),
            this,
            useSynchronizationContext: false);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            this._parentReg.Dispose();
        }

        base.Dispose(disposing);
    }
}

还有一个演示:

CancellationTokenSource[] CreateChildSources(CancellationTokenSource parentSource) =>
    Enumerable.Range(0, 2)
        .Select(_ => new HierarchicalCancellationTokenSource(parentSource.Token))
        .ToArray();

var rootSource = new CancellationTokenSource();
var childSources = CreateChildSources(rootSource);
var grandChildSources = childSources.SelectMany(CreateChildSources).ToArray();

var allTokens = new[] { rootSource.Token }
    .Concat(childSources.Select(s => s.Token))
    .Concat(grandChildSources.Select(s => s.Token))
    .ToArray();

for (int i = 0; i < allTokens.Length; i++)
{
    allTokens[i].Register(
        i => Console.WriteLine(
            $"{new string('+', (int)Math.Log2((int)i))}{i} canceled."),
        i + 1);
}

rootSource.Cancel();

/* Output:
1 canceled.
+3 canceled.
++7 canceled.
++6 canceled.
+2 canceled.
++5 canceled.
++4 canceled.
*/