0

I am trying to cancel a background worker if its currently running, and then start another.

I tried this first, there are more checks for cancel in the functions...

    private void StartWorker()
    {
        if (StartServerGetIP.IsBusy) { StartServerGetIP.CancelAsync(); }
        StartServerGetIP.RunWorkerAsync();
    }
 private void StartServerGetIP_DoWork(object sender, DoWorkEventArgs e)
    {
        StartFTPServer(Port, Ringbuf, sender as BackgroundWorker, e);
        if ((sender as BackgroundWorker).CancellationPending) return;
        GetIP(Ringbuf, sender as BackgroundWorker, e);
    }

private void StartServerGetIP_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Cancelled)
        {
            return;
        }
        if (e.Result.ToString() == "Faulted")
        {
            tcs.SetResult(false);
            return;
        }

        Client.IPAddress = e.Result.ToString();
        tcs.SetResult(true);
    }

This approach blocks if the worker is canceled on StartServerGetIP.RunWorkerAsync();

After this I found an ugly solution in

        private void StartWorker()
    {
        if (StartServerGetIP.IsBusy) { StartServerGetIP.CancelAsync(); }
        while(StartServerGetIP.IsBusy) { Application.DoEvents(); }

        StartServerGetIP.RunWorkerAsync();
    }

Is there a pattern I can implement that will allow me to async cancel the background worker and start another without calling Application.DoEvents?

EDIT: A cancel button is out of the question.

EDIT: For those asking about the inner methods...

private void StartFTPServer(SerialPort port, RingBuffer<string> buffer, BackgroundWorker sender, DoWorkEventArgs args)
    {
        Stopwatch timeout = new Stopwatch();
        TimeSpan max = TimeSpan.FromSeconds(MaxTime_StartServer);
        int time_before = 0;
        timeout.Start();
        while (!buffer.Return.Contains("Run into Binary Data Comm mode...") && timeout.Elapsed.Seconds < max.Seconds)
        {
            if (timeout.Elapsed.Seconds > time_before)
            {
                time_before = timeout.Elapsed.Seconds;
                sender.ReportProgress(CalcPercentage(max.Seconds, timeout.Elapsed.Seconds));
            }
            if (sender.CancellationPending)
            {
                args.Cancel = true;
                return;
            }
        }
        port.Write("q"); //gets into menu
        port.Write("F"); //starts FTP server
    }

        private void GetIP(RingBuffer<string> buffer, BackgroundWorker sender, DoWorkEventArgs args)
    {
        //if longer than 5 seconds, cancel this step
        Stopwatch timeout = new Stopwatch();
        TimeSpan max = TimeSpan.FromSeconds(MaxTime_GetIP);
        timeout.Start();
        int time_before = 0;
        string message;

        while (!(message = buffer.Return).Contains("Board IP:"))
        {
            if (timeout.Elapsed.Seconds > time_before)
            {
                time_before = timeout.Elapsed.Seconds;
                sender.ReportProgress(CalcPercentage(max.Seconds, timeout.Elapsed.Seconds + MaxTime_StartServer));
            }
            if (timeout.Elapsed.Seconds >= max.Seconds)
            {
                args.Result = "Faulted";
                return;
            }
            if (sender.CancellationPending)
            {
                args.Cancel = true;
                return;
            }
        }
        Regex regex = new Regex(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b");
        string IP = message.Remove(0, "Board IP: ".Length);
        if (regex.IsMatch(IP))
        {
            args.Result = IP;
            ServerAlive = true;
        }
    }

Might as well give you the ring buffer too..

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FPGAProgrammerLib
{
    class RingBuffer<T>
    {
        T [] buffer { get; set; }

        int _index;
        int index
        {
            get
            {
                return _index;
            }
            set
            {
                _index = (value) % buffer.Length;
            }
        }


        public T Add
        {
            set
            {
                buffer[index++] = value;
            }
        }

        public T Return
        {
            get
            {
                return (index == 0) ? (IsString() ? (T)(object)string.Empty : default(T)) : buffer[--index];
            }
        }

        private bool IsString()
        {
            return (typeof(T) == typeof(string) || (typeof(T) == typeof(String)));
        }

        public RingBuffer(int size)
        {
            buffer = new T[size];
            index = 0;
        }
    }
}
Azymptote
  • 1
  • 2
  • Don't ever use `Application.DoEvents()`. It's fraught with danger. Unless you are very careful/lucky it will cause extremely difficult to debug errors in your code. It's only in the framework for backward compatibility for VB6 upgrade. – Enigmativity Oct 28 '17 at 00:44
  • There are far better ways to do this. Can you post the full code? I would like to be able to copy, paste, and run your code. – Enigmativity Oct 28 '17 at 00:45
  • It is *always* a fundamental race the way you are doing it now. You have no guarantee that CancelAsync() actually ensures that tcs.Result won't be set. Undebuggable as well, this only goes wrong once a week. There is not enough code to propose a correct solution, as-is you must either fail StartWorker() or ensure it can never be called. – Hans Passant Oct 28 '17 at 10:08

2 Answers2

0

In your StartServerGetIP_DoWork method there's a StartFTPServer method. I assume you don't check in that method if a cancellation has been requested. The same thing applies to your GetIP method. Those are probably your blocking points. If you want to ensure to actually cancel the job, you need to check periodically if a cancellation has been requested. So I would suggest you use an async method for StartFTPServer and GetIP that will check if the background worker has a cancellation requested.

I don't know the exact implementation you did in the StartFTPServer method or the GetIP method. If you would like more details on how to refactor the code so it can be cancelled post the code.

SniperLegacy
  • 139
  • 5
0

Here's a simple way to effectively cancel an in-flight function that's operating on another thread by using Microsoft's Reactive Framework (Rx).

Start with a long-running function that returns the value you want:

Func<string> GetIP = () => ...;

And you have some sort of trigger - could be a button click or a timer, etc - or in my case I'm using a type from the Rx library.

Subject<Unit> trigger = new Subject<Unit>();

Then you can write this code:

IObservable<string> query =
    trigger
        .Select(_ => Observable.Start(() => GetIP()))
        .Switch()
        .ObserveOn(this);

IDisposable subscription =
    query
        .Subscribe(ip => { /* Do something with `ip` */ });

Now anytime that I want to initiate the function I can call this:

trigger.OnNext(Unit.Default);

If I initiate a new call while an existing call is running the existing call will be ignored and only the latest call (so long as it completes) will end up being produced by the query and the subscription will get it.

The query keeps running and responds to every trigger event.

The .ObserveOn(this) (assuming you're using WinForms) brings the result back on to the UI thread.

Just NuGet "System.Reactive.Windows.Forms" to get the bits.

If you want trigger to be a button click, do this:

IObservable<Unit> trigger =
    Observable
        .FromEventPattern<EventHandler, EventArgs>(
            h => button.Click += h,
            h => button.Click -= h)
        .Select(_ => Unit.Default);
Enigmativity
  • 113,464
  • 11
  • 89
  • 172