Search code examples
c#taskcancellationtokensourcecancellation-token

CancellationToken.ThrowIfCancellationRequested not throwing


I've written a very simple app to implement some task-based asynchronous operation.

The client code calls a method which returns a Task. I pass a CancellationToken to that method but even if I call CancellationToken.ThrowIfCancellationRequested method during the process, cancelling never throws OperationCancelledException.

You can download the whole solution here, if you want to test for yourself : https://github.com/stevehemond/asynctap-example

Here is the code :

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace AsyncTapExample
{
    public partial class MainForm : Form
    {
        private const int totalSeconds = 5;

        private bool isStarted;

        public MainForm()
        {
            this.InitializeComponent();
        }

        private async void processingButton_Click(object sender, EventArgs e)
        {
            var cts = new CancellationTokenSource();

            if (!this.isStarted)
            {
                this.processingButton.Text = "Cancel";

                this.isStarted = true;

                var progressIndicator = new Progress<int>(this.ReportProgress);

                try
                {
                    await this.ProcessLongRunningOperationAsync(progressIndicator, cts.Token);

                    MessageBox.Show("Completed!");
                }
                catch (OperationCanceledException)
                {
                    MessageBox.Show("Cancelled!");
                }

                this.ResetUI();
            }
            else
            {
                cts.Cancel();
                this.processingButton.Text = "Start";
                this.isStarted = false;
            }
        }

        private void ResetUI()
        {
            this.progressBar.Value = 0;
            this.processingButton.Enabled = true;
            this.progressMessageLabel.Text = string.Empty;
            this.isStarted = false;
            this.processingButton.Text = "Start";
        }

        private Task ProcessLongRunningOperationAsync(IProgress<int> progress, CancellationToken ct)
        {
            return Task.Run(
                () =>
                    {
                        for (var i = 0; i <= totalSeconds; i++)
                        {
                            ct.ThrowIfCancellationRequested();

                            Thread.Sleep(1000);
                            progress?.Report((i * 100) / totalSeconds);
                        }
                    },
                ct);
        }

        private void ReportProgress(int progressPercentage)
        {
            this.progressBar.Value = progressPercentage;
            this.progressMessageLabel.Text = $"{progressPercentage}%";
        }
    }
}

There must be something I don't understand about passing the CancellationToken to Tasks ... I just can't figure out what.


Solution

  • You create CancellationTokenSource on every call to processingButton_Click. In result you cancel different CancellationTokenSource from what used to create task. You should create new CancellationTokenSource only when you create new task, and you should save that CancellationTokenSource, so you can cancel it:

    private CancellationTokenSource cts; //MainForm instance field
    
    private async void processingButton_Click(object sender, EventArgs e)
    {
        if (!this.isStarted)
        {
            this.cts = new CancellationTokenSource();
    
            this.processingButton.Text = "Cancel";
    
            this.isStarted = true;
    
            var progressIndicator = new Progress<int>(this.ReportProgress);
    
            try
            {
                await this.ProcessLongRunningOperationAsync(progressIndicator, this.cts.Token);
    
                MessageBox.Show("Completed!");
            }
            catch (OperationCanceledException)
            {
                MessageBox.Show("Cancelled!");
            }
    
            this.ResetUI();
        }
        else
        {
            this.cts.Cancel();
            this.processingButton.Text = "Start";
            this.isStarted = false;
        }
    }