Search code examples
c#invokecancellationtokensource

CancellationTokenSource misbehaviour


I have a problem when i wait for a task after i have canceled it with the CancellationTokenSource. The cancel call does not interrupt the task. When i wait for the task the main thread blocks because the task will be never interrupted.

Here is a short description my program: A task increments a char variable (from 'A' to 'Z') and shows it on the GUI thread. In order to do this the task executes a delegate (this.invoke()) on the thread the control was created on.

As soon as i comment out the RefreshTextBox()-Function the cancel call works and the task will be interrupted. It seems as if the this.invoke() command prevents the task from interrupting.

I the code below i have also implemented the same functionality with normal threads. And then i works. Where is the difference between task implementation and thread implementation?

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

public partial class frm_Main : Form
{
    private delegate void dgt_StringHandler(string str_Value);
    CancellationTokenSource _obj_Cts = null;
    Thread _obj_Thread = null;
    Task _obj_Task = null;

    public frm_Main()
    {
        InitializeComponent();
    }

    private void CreateChar(ref char chr_Value)
    {
        int int_Value;

        int_Value = (int)chr_Value;
        int_Value++;

        if (int_Value > 90 || int_Value < 65)
            int_Value = 65;

        chr_Value = (char)int_Value;
    }

    private void TestThread()
    {
        char chr_Value = '@';
        bool bol_Stop = false;

        while (!bol_Stop)
        {
            try
            {
                Thread.Sleep(300);
                CreateChar(ref chr_Value);
                RefreshTextBox(chr_Value.ToString());
            }
            catch (ThreadInterruptedException)
            {
                bol_Stop = true;
            }
        }
    }

    private void TestTask(object obj_TokenTmp)
    {
        char chr_Value = '@';
        CancellationToken obj_Token = (CancellationToken)obj_TokenTmp;

        while (!obj_Token.IsCancellationRequested)
        {
            Thread.Sleep(300);
            CreateChar(ref chr_Value);
            RefreshTextBox(chr_Value.ToString());
        }
    }

    private void RefreshTextBox(string str_Value)
    {
        if (txt_Value.InvokeRequired)
        {
            dgt_StringHandler obj_StringHandler = new dgt_StringHandler(RefreshTextBox);
            this.Invoke(obj_StringHandler, new object[] { str_Value });
        }
        else
        {
            txt_Value.Text = str_Value;
        }
    }

    private void btn_StartStop_Click(object sender, EventArgs e)
    {
        if (_obj_Task == null && _obj_Thread == null)
        {
            if (opt_Task.Checked)
            {
                _obj_Cts = new CancellationTokenSource();
                _obj_Task = new Task(new Action<object>(TestTask), _obj_Cts.Token, _obj_Cts.Token);
                _obj_Task.Start();
            }
            else
            {
                _obj_Thread = new Thread(new ThreadStart(TestThread));
                _obj_Thread.Start();
            }

            btn_StartStop.Text = "Stop";
        }
        else
        {
            if (_obj_Thread != null)
            {
                _obj_Thread.Interrupt();
                _obj_Thread.Join();
                _obj_Thread = null;
            }

            if (_obj_Task != null)
            {
                _obj_Cts.Cancel();
                _obj_Task.Wait();
                _obj_Task = null;
                _obj_Cts = null;
            }

            btn_StartStop.Text = "Start";
        }
    }
}

Solution

  • These 2 pieces of the code together form a deadlock:

    _obj_Cts.Cancel();
    _obj_Task.Wait();
    

    and

    this.Invoke(obj_StringHandler, new object[] { str_Value });
    

    You are calling Wait() on the main thread, and Invoke() needs to be handled by the main thread.

    You can break the deadlock by using this.BeginInvoke(...) instead.

    The Thread version uses Interrupt, a sledgehammer. So the thread won't try to call RefreshTextBox() after the stop signal.