1

Suppose I have a long operation inside a subroutine or function and I want to be able to cancel (exit subroutine or function) immediately after a "cancellation flag" is set to true. What is the best way to do it? One way is to check the flag after each line of code but that is not very elegant.

For example:

dim _CancelFlag as boolean = false

Sub LongOperation()
    dim a as integer

    a = 1
    if _CancelFlag = True Then
        Exit Sub
    End If

    a = 2
    if _CancelFlag = True Then
        Exit Sub
    End If

    'And so on...
End Sub

Of course a = 1 is only for illustration purpose. Say the operation is really long until a = 100 and it is not possible to put them into a loop, how can I trigger the cancellation from outside of the subroutine and stop it immediately?

I was thinking to put the sub into a BackgroundWorker or Task but then I still have to check for CancellationToken somewhere inside the sub.. Do I really have to check after each line of code?

Albert Tobing
  • 169
  • 2
  • 14
  • 1
    Does this answer your question? [Proper way of cancel execution of a method](https://stackoverflow.com/questions/13482895/proper-way-of-cancel-execution-of-a-method) – Lance U. Matthews Feb 21 '20 at 07:20
  • Yes you have to. the `BackgroundWorker` to do the lengthy routine, and the main thread to keep the UI responsive including the button for example that will `cancel` the task by setting the `CancelFlag = True` in it's `Click` event. –  Feb 21 '20 at 07:24
  • yes, but then the cancellation flag need to be captured inside the backgroundworker DoWork and cancel from inside right? If the sub routine above is inside the BackgroundWorker's DoWork, where should I check the: if worker.CancellationPending = True ? – Albert Tobing Feb 21 '20 at 08:16
  • all the cancellation token example is inside loop. the cancellation flag is checked at the end of the loop. this is different from my question. – Albert Tobing Feb 21 '20 at 08:18
  • 1
    The whole point is that you shouldn't be doing what you're trying to do. You can `Abort` a thread but it is generally recommended against because it leaves the system in an unknown state. That reason that you should be checking and cancelling explicitly is so that you know that you've cleaned up whatever needs it. – jmcilhinney Feb 21 '20 at 08:37
  • Aborting a thread is a bad thing. First the abortion semantics is different than what is generally expected, and, above, all aborting a thread that is holding handles may have unintended consequences and doom the handles. If you need to logically abort a task, you have to design your code to achieve that goal in a clean way. – Yennefer Feb 21 '20 at 09:12
  • that's why I mentioned checking on flag to abort the operation which is the standard way of background worker and Task. The question is, should I do the flag checking on per line basis to achieve the fastest "reaction" time upon cancellation or is there any other way that I don't know? – Albert Tobing Feb 21 '20 at 09:56
  • inside loop it is much simpler. the flag checking can be done on each iteration. 1x execution followed by 1x flag checking. But when it is not possible to use loop, the only way I can think of to get the same result is to put the flag checking operation line by line. – Albert Tobing Feb 21 '20 at 10:05

4 Answers4

2

It depends on the granularity you want to achieve: how many seconds can you expect your method be canceled?

If the cancellation must take place "immediately" you have to check in as many place as you can. However, just checking before and after long sub steps of your operation is enough in the general case.

Remember that if you have to wait on handles, you have to use the appropriate overload that specifies a timeout or a cancellation token.

Additionally, you should propagate the cancellation token/your flag deep down your methods to allow detection early the cancellation requests.

Yennefer
  • 5,704
  • 7
  • 31
  • 44
  • in other words, check after each line of code for finest granularity right? – Albert Tobing Feb 21 '20 at 08:12
  • Check after and before lines that invoke long operations which you have not in control. For example, assigning variables virtually have no cost, therefore it is not useful to check after every assignment. – Yennefer Feb 21 '20 at 09:09
  • (If I am not clear enough, kindly post an example of a code yours and I'll show you what I was talking about) – Yennefer Feb 21 '20 at 09:13
  • yes, I know. I have several loop inside the operation and a lot of low cost operations. I will definitely check cancel flag in loop. The question is the low cost operation. It doesn't cost much time but in my case, it is better to cancel sooner when cancel flag is on. it's plc io operation. it's fast but better if can stop the operation as soon as cancel flag is detected. – Albert Tobing Feb 21 '20 at 10:56
  • I agree. Just one last thing: if you use the flag and multiple threads, be sure to use the volatile pattern. – Yennefer Feb 21 '20 at 12:43
0

I found a more elegant way to do it, although it does use a loop in the end. Please let me know if anybody has an better solution. I will also update when I find something else.

Sub LongOperation()
    dim state as integer = 0

    Do while state < 100
        Select Case state
            Case 0
                a = 1
            Case 1
                a = 2
            Case Else
                Exit do
        End Select

        If _CancelFlag = True Then
            Exit Sub
        End If

        state += 1
    Loop

End Sub
Albert Tobing
  • 169
  • 2
  • 14
-1

This is a sample windows application I have created to cancel or pause the log running task.

public partial class Form1 : Form
    {
        updateUI _updateGUI;
        CancellationToken _cancelToken;
        PauseTokenSource _pauseTokeSource;
        public Form1()
        {
            InitializeComponent();
        }


        delegate void updateUI(dynamic value);

        private void btnStartAsync_Click(object sender, EventArgs e)
        {
            _pauseTokeSource = new PauseTokenSource();
            _cancelToken = default(CancellationToken);

            _pauseTokeSource.onPause -= _pauseTokeSource_onPause;
            _pauseTokeSource.onPause += _pauseTokeSource_onPause;

            Task t = new Task(() => { LongRunning(_pauseTokeSource); }, _cancelToken);
            t.Start();

        }

        private void _pauseTokeSource_onPause(object sender, PauseEventArgs e)
        {
            var message = string.Format("Task {0} at {1}", e.Paused ? "Paused" : "Resumed", DateTime.Now.ToString());
            this.Invoke(_updateGUI, message);
        }

        private async void LongRunning(PauseTokenSource pause)
        {
            _updateGUI = new updateUI(SetUI);
            for (int i = 0; i < 20; i++)
            {

                await pause.WaitWhilePausedAsync();

                Thread.Sleep(500);
                this.Invoke(_updateGUI, i.ToString() + " => " + txtInput.Text);

                //txtOutput.AppendText(Environment.NewLine + i.ToString());


                if (_cancelToken.IsCancellationRequested)
                {
                    this.Invoke(_updateGUI, "Task cancellation requested at " + DateTime.Now.ToString());
                    break;
                }
            }
            _updateGUI = null;
        }


        private void SetUI(dynamic output)
        {
            //txtOutput.AppendText(Environment.NewLine + count.ToString() + " => " + txtInput.Text);
            txtOutput.AppendText(Environment.NewLine + output.ToString());
        }

        private void btnCancelTask_Click(object sender, EventArgs e)
        {
            _cancelToken = new CancellationToken(true);
        }

        private void btnPause_Click(object sender, EventArgs e)
        {
            _pauseTokeSource.IsPaused = !_pauseTokeSource.IsPaused;
            btnPause.Text = _pauseTokeSource.IsPaused ? "Resume" : "Pause";
        }
}

 public class PauseTokenSource
    {
        public delegate void TaskPauseEventHandler(object sender, PauseEventArgs e);
        public event TaskPauseEventHandler onPause;

        private TaskCompletionSource<bool> _paused;
        internal static readonly Task s_completedTask = Task.FromResult(true);

        public bool IsPaused
        {
            get { return _paused != null; }
            set
            {

                if (value)
                {
                    Interlocked.CompareExchange(ref _paused, new TaskCompletionSource<bool>(), null);
                }
                else
                {
                    while (true)
                    {
                        var tcs = _paused;
                        if (tcs == null) return;
                        if (Interlocked.CompareExchange(ref _paused, null, tcs) == tcs)
                        {
                            tcs.SetResult(true);
                            onPause?.Invoke(this, new PauseEventArgs(false));
                            break;
                        }
                    }
                }
            }
        }

        public PauseToken Token
        {
            get
            {
                return new PauseToken(this);
            }
        }

        internal Task WaitWhilePausedAsync()
        {
            var cur = _paused;

            if (cur != null)
            {
                onPause?.Invoke(this, new PauseEventArgs(true));
                return cur.Task;
            }
            return s_completedTask;
        }
    }

    public struct PauseToken
    {
        private readonly PauseTokenSource m_source;
        internal PauseToken(PauseTokenSource source) { m_source = source; }

        public bool IsPaused { get { return m_source != null && m_source.IsPaused; } }

        public Task WaitWhilePausedAsync()
        {
            return IsPaused ?
                m_source.WaitWhilePausedAsync() :
                PauseTokenSource.s_completedTask;
        }
    }

    public class PauseEventArgs : EventArgs
    {
        public PauseEventArgs(bool paused)
        {
            Paused = paused;
        }
        public bool Paused { get; private set; }
    }
Neeraj Kumar Gupta
  • 2,157
  • 7
  • 30
  • 58
  • Thank you for sharing but I know this logic already. Your long running task is inside a for loop. In that case, cancellation token is checked every 500ms in your loop. What I am looking is when it is not inside a loop and cancel immediately, if possible. – Albert Tobing Feb 21 '20 at 08:11
  • @AlbertTobing, no prob bro – Neeraj Kumar Gupta Feb 21 '20 at 09:54
  • 1
    The majority of the code presented here relates to _pausing_ a task, which was not part of the question. You could have trimmed this down to the key lines `for (int i = 0; i < 20; i++) { Thread.Sleep(500); if (_cancelToken.IsCancellationRequested) { } }`, but then the question specifically stated "it is not possible to put [the steps of the task] into a loop." So, as much code as was given here, none of it really applies to or answers the question. – Lance U. Matthews Feb 21 '20 at 19:04
-1

If your LongOperation() is well splittable into short operations (I assume a=1, a=2, ..., a=100 being all reasonably short) than you could wrap all the short operations into Tasks, put them into a TaskQueue and process that queue, checking between the Tasks if cancellation was requested.

If LongOperation() is difficult to split you could run the LongOperation() on a separate dedicated thread and abort that thrad on cancellation. Some have commented aborting a thread being dirty and not being recommended. Actually that's not that bad, if properly handled. Aborting a thread just raises a ThradAbortException within the thread method. So if there is a try - catch - finally in the LongOperation(), catching and handling the exception and if the finally code properly does cleanup, closes all handles, disposes etc., this should be ok and nothing to be afraid of.

protix
  • 99
  • 2
  • 1
    Except `Thread.Abort()` **is** [that bad](https://stackoverflow.com/q/1559255/150605). Among other issues, when you abort a thread you'll have no idea where in the long-running task it was canceled, unless you wrap every step in `try { ... } catch (ThreadAbortException) { ... }`, in which case we're back where the question started. As [Destroying threads](https://learn.microsoft.com/dotnet/standard/threading/destroying-threads) states, `Abort()` is for stopping code "not designed for cooperative cancellation." The real solution? _Design your code for cooperative cancellation._ – Lance U. Matthews Feb 21 '20 at 18:40
  • As you can see, my first suggstion was for a cooperation cancellation design ;) – protix Feb 22 '20 at 22:03
  • And only if not possible `Thread.Abort()`, and in that case, of course, every single step or the whole `LongOperation()` has to be wrapped in `try { ... } catch { ... } finally { ... }` blocks. I know all those examples, but they seem to revolve around not properly handling within the aborted thread (including `Thread.BeginCriticalReagion()` and so on ...). Can you give an example, how `Thread.Abort()` can hurt, when full and correct cleanup and state management handling in the aborted thread is done? – protix Feb 22 '20 at 22:17
  • "only if not possible" - Except it _is_ possible. Why is `Abort()` even in the conversation when we _know_ the author controls both the canceler and the cancelee? Further, you now mention the need for things like "`Thread.BeginCriticalReagion()` and so on" (without defining "so on"), so can you see how this answer understates the ramifications of `Abort()`? The issue isn't if `Abort()` can possibly be used safely, the issue is if it's as simple and "nothing to be afraid of" as you make it out to be. Does jumping through all these hoops really sound anything like the "best way" here, anyways? – Lance U. Matthews Feb 24 '20 at 21:19