How to Create a Hierarchical CancellationTokenSource?
Asked Answered
N

2

7

In our project, we have decided to provide a cancellation mechanism for users with the help of CancellationToken.

Because of the structure of the works in the project, I need a hierarchical cancellation mechanism. By hierarchical, I mean that parent source cancellation causes all child sources to be recursively canceled but child sources cancellations are not propagated to the parent.

Is there such an option available in .NET out of the box? If not, I'm not sure whether registering a delegate to the parent token is enough or further considerations should be given.

Nowt answered 10/10, 2021 at 15:5 Comment(0)
N
3

Based on the implementation in the source code for Linked2CancellationTokenSource, I came to this implementation:

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

And a demo:

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.
*/
Nowt answered 11/10, 2021 at 7:22 Comment(1)
recommendation: try this._parentReg = parentToken.Register(static s => ((CancellationTokenSource)s).Cancel(false), this, false); - it avoids creating 2 additional objects per registration (the capture context instance, and a delegate instance) - instead, the instance (this) is passed as state, and the delegate will be hoisted and reused via a compiler-generated static fieldTranscend
C
10

Yes, this functionality exists out of the box. Check out the CancellationTokenSource.CreateLinkedTokenSource method.

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

Example:

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
Commines answered 10/10, 2021 at 15:21 Comment(7)
Thanks for your answer. Is it possible to extend CancellationTokenSource to support this behavior internally? It seems that Token and Cancel are not overridable.Nowt
@momt99 why do you want to override them? What are you trying to achieve there, that cannot already be achieved?Transcend
@momt99 no, the only overridable member of the CancellationTokenSource class is the Dispose(bool disposing) method.Commines
@MarcGravell I was hoping to extend CancellationTokenSource to provide such a behavior.Nowt
@momt99 such a behavior as already exists?Transcend
@MarcGravell actually it's a bit different in terms of construction. By the way, I have posted my answer based on the linked token source. I would be glad if you could review it.Nowt
@momt99 ah, you're inverting the parent/child direction; fine - if that works for you: greatTranscend
N
3

Based on the implementation in the source code for Linked2CancellationTokenSource, I came to this implementation:

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

And a demo:

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.
*/
Nowt answered 11/10, 2021 at 7:22 Comment(1)
recommendation: try this._parentReg = parentToken.Register(static s => ((CancellationTokenSource)s).Cancel(false), this, false); - it avoids creating 2 additional objects per registration (the capture context instance, and a delegate instance) - instead, the instance (this) is passed as state, and the delegate will be hoisted and reused via a compiler-generated static fieldTranscend

© 2022 - 2024 — McMap. All rights reserved.