如何创建一个分层的CancellationTokenSource?

7
在我们的项目中,我们决定通过 CancellationToken 来为用户提供取消机制。由于项目中工作的结构,我需要一个分层的取消机制。所谓分层,是指父源取消将递归地导致所有子源被取消,但子源的取消不会传播到父源。
在 .NET 中是否有这样的选项可用?如果没有,我不确定在父令牌上注册委托是否足够,还需要进一步考虑。
2个回答

10

是的,此功能可以直接使用。请查看CancellationTokenSource.CreateLinkedTokenSource 方法。

创建一个 CancellationTokenSource 对象,当源令牌中的任何一个令牌处于取消状态时,该对象将自动处于取消状态。

示例:

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

感谢您的回答。是否可以扩展CancellationTokenSource以在内部支持此行为?似乎TokenCancel不可重写。 - momvart
@momt99 为什么你想要覆盖它们?你想要实现什么目标,而这个目标又不能通过其他方式实现吗? - Marc Gravell
@momt99 这种行为已经存在了吗? - Marc Gravell
@MarcGravell,实际上在构造方面有些不同。顺便说一下,我已经根据链接的令牌源发布了我的答案。如果您能够审核一下,我会很高兴的。 - momvart
1
@momt99 啊,你反转了父/子方向;没问题 - 如果这对你有效:太好了。 - Marc Gravell
显示剩余2条评论

3

根据Linked2CancellationTokenSource的源代码实现,我得到了以下的实现:

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.
*/

2
建议尝试使用 this._parentReg = parentToken.Register(static s => ((CancellationTokenSource)s).Cancel(false), this, false); - 这样可以避免每次注册时创建两个额外的对象(捕获上下文实例和委托实例)- 相反,实例(this)将作为状态传递,并且委托将通过编译器生成的静态字段升级并重用。 - Marc Gravell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接