Search code examples
c#task-parallel-librarytaskcancellationtokensourcecancellation-token

C# Task - Linked cancellation token not working


Please if someone could help me. I'm trying to use TPL linked cancellation tokens. Issue is that after cancellation of the main CancellationTokenSource, value of linked token's property IsCancellationRequested is still "false".

I'm starting two tasks, just to be sure - but it should be the same thing. To the first I pass CancellationToken, and to the second I pass CancellationTokenSource. The behaviour is the same: In the while loops - condition linkedToken.IsCancellationRequested stays "false" after cancellation.

Here is the code I am using:

public class Manager
{
    private Task tokenTask;
    private Task sourceTask;
    private CancellationTokenSource mainCancelationTokenSource;
    private CancellationToken mainToken;

    public Manager()
    {
        this.mainCancelationTokenSource = new CancellationTokenSource();
        this.mainToken = mainCancelationTokenSource.Token;
        this.mainToken.Register(MainCanceled);
    }

    public void Start()
    {
        Workers w = new Workers();
        tokenTask = Task.Run(() => w.DoWorkToken(mainToken), mainToken);
        sourceTask = Task.Run(() => w.DoWorkSource(mainCancelationTokenSource), mainCancelationTokenSource.Token);

    }
    public void Cancel()
    {
        mainCancelationTokenSource.Cancel();
    }

    private void MainCanceled()
    {
        try
        {
            tokenTask.Wait();
        }
        catch (Exception e)
        {

        }

        try
        {
            sourceTask.Wait();
        }
        catch (Exception e)
        {

        }
    }
}

class Workers
{
    public void DoWorkToken(CancellationToken mainToken)
    {
        CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(mainToken);
        CancellationToken linkedToken = linkedCts.Token;

        while (!linkedToken.IsCancellationRequested)
        {
            Random r = new Random();
            Task.Delay(200 * r.Next(1, 11)).Wait();
        }

        linkedToken.ThrowIfCancellationRequested();
    }

    public void DoWorkSource(CancellationTokenSource mainCts)
    {
        CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(mainCts.Token);

        while (!linkedCts.Token.IsCancellationRequested)
        {
            Random r = new Random();
            Task.Delay(200 * r.Next(1, 11)).Wait();
        }

        linkedCts.Token.ThrowIfCancellationRequested();
    }
}

To start this code from a console application Main method:

class Program
{
    static void Main(string[] args)
    {
            Manager manager = new Manager();
            manager.Start();
            //Console.ReadKey();

            Thread.Sleep(5000);
            manager.Cancel();
   }
}

Thank you for your help!


Solution

  • The source of this problem is this line:

    this.mainToken.Register(MainCanceled);
    

    You register a callback to execute when token is cancelled. Internally that callback is delegated to the parent CancellationTokenSource and is put in some list of callbacks to execute when source cancellation is requested. In that handler you do

    tokenTask.Wait();
    sourceTask.Wait();
    

    So this callback will not be completed until related tasks are completed. In tasks you do this

    while (!linkedToken.IsCancellationRequested) {
        // loop here
    }
    

    So tasks will not be completed until cancellation is requested.

    Now to the tricky part: when you create linked token source

    CancellationTokenSource.CreateLinkedTokenSource(mainToken)
    

    It will also put a callback in the list of parent (mainToken) token source. When this callback is executed - linked token source becomes cancelled too. This callback will be after your callback in the list.

    So you are deadlocked, because first callback (yours) waits for tasks to complete, tasks wait for linked token IsCancellationRequested equals true, and linked token source waits for it's own callback to complete to set this flag.

    To solve this issue, just remove the callback, or write it in a way that it does not block waiting for related task to complete, or change tasks to not wait on linked token source IsCancellationRequested flag.