2

I have a WinForms application on .NET 3.5. In this form, the user triggers an operation which is executed in another thread (a BackgroundWorker to be precise) so as to not block the UI thread. I'm in MVP, so all this is being done by a presenter which interacts with an interface to the view (implemented by the Windows Form). So far so good.

I would like to introduce functionality whereby a timeout period is introduced for the background operation to complete before cancelling it. Sounds simple enough. But the background operation calls a single function on a third-party component which may never return, so the cancellation capabilities of the BackgroundWorker are of no use to me here. Also, the BackgroundWorker.RunWorkerCompleted allowed me to get back on the UI thread, so I need to wait for the timeout or success and be able to get back to my calling thread (namely the UI thread).

I tried this using a plain old Thread (which does support Abort()) and a Timer running on a second thread, but can't seem to get it to work quite right since Join() is blocking my UI thread despite the description stating that it will block "while continuing to perform standard COM and SendMessage pumping". Admittedly I assumed this implied that it would continue to process Windows Messages, which was not the case.

int timeoutInMsec = 10000;
Thread connectThread = new Thread(Connect);
Thread timerThread = new Thread(() =>
    {
        var timer = new System.Windows.Forms.Timer() { Interval = timeoutInMsec };
        timer.Tick += (_s, _e) => 
            {
                timer.Stop();
                if (connectThread.ThreadState == ThreadState.Running)
                    connectThread.Abort();
            };
    };

connectThread.Start();
timerThread.Start();
timerThread.Join();
connectThread.Join();

Based on this question, I tried removing the second timer thread and adding a ManualResetEvent and calling Set() when the timer ticked, or when the Connect method did indeed complete. Here, instead of Join I used WaitOne, but unfortunately this also blocks my UI thread. I also found this other question, which a CancellationTokenSource which unfortunately is not available in .NET 3.5.

So, how can I spin my worker up and be able to terminate it after a given amount of time in .NET 3.5, while at the same time be able to get back to the thread where I spun up the worker thread to execute a sort of OnCompleted handler?

Many thanks in advance!

PS: I don't have a lot of experience in multi-threaded programming in .NET, so I'm sorry if this is trivial.

Community
  • 1
  • 1
user5877732
  • 371
  • 3
  • 19
  • You should never ever call `Thread.Abort()` as it can corrupt the run-time state. The **only** exception is if you are trying to force the shutdown of your app. The only way to cleanly kill a thread without compromising your run-time is to launch a new process and run your thread there, then you can just kill the process. – Enigmativity Mar 03 '17 at 13:02
  • 1
    @Enigmativity: The *never use* `Thread.Abort` is a similar blind argument as never use `Application.DoEvents`. They are often misused, indeed. But it does not mean you cannot use them properly. See their description, they are neither deprecated nor obsolete. – György Kőszeg Mar 03 '17 at 14:47
  • To the downvoter: be a chap and let me know how the question can be improved – user5877732 Mar 03 '17 at 15:29
  • @taffer - No, they are different arguments. The `Thread.Abort()` argument is that you can corrupt the state of the run-time - that's very bad. `Application.DoEvents()` can merely cause re-entrancy bugs that are hard to fix, but it doesn't corrupt the run-time state. The former is far worse than the latter. – Enigmativity Mar 04 '17 at 07:37

2 Answers2

1

If I understood your question correctly, the following algorithm should solve your problem:

  • As before, create a BackgroundWorker to do your background work.

  • In BackgroundWorker_DoWork,

    • create a new thread (let's call it the "third-party thread") to call your third-party library, and then
    • wait for the third-party thread to finish or the timeout to elapse. (*)

That way, your UI won't block, since only the Backgroundworker thread is waiting, not the main thread.

Now about the interesting part: How do you wait for the third-party thread to finish (the step marked with (*))?

My suggestion would be to simply use "loop waiting with sleep", i.e. (pseudo-code, you can use the Stopwatch class for the timeout):

do until (third-party thread has finished or x seconds have elapsed):
    Thread.Sleep for 100ms

if third-party thread has not finished:
    Abort it     // we don't have another choice
else
    Process the result

It's not best practice, but it's simple, it gets the job done and you can always replace it with fancy cross-thread-syncronization stuff (which is non-trivial to get right) once you got it all working.

Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • 1
    Duh. Of course I don't care if I block the worker thread... Interestingly, calling `Abort` results in the application process not exiting when closing the form... – user5877732 Mar 03 '17 at 14:01
  • 2
    @user5877732: Be sure to set `IsBackground` to true, see http://stackoverflow.com/a/2689000/87698 – Heinzi Mar 03 '17 at 14:24
-1

It's useless to create a Forms.Timer on a non-gui thread. Don't create it on a separate thread. Why are you Joining the threads? The usage of Join is to block the current thread until the other thread is finished.

This is untested pseudo code, this is for example purpose.

public class Form1: Form1
{
    private int timeoutInMsec = 10000;
    private System.Windows.Forms.Timer _timer;
    private Thread _connectThread;

    public Form1()
    {
        _connectThread = new Thread(Connect);
        _connectThread.Start();

        _timer = new System.Windows.Forms.Timer() { Interval = timeoutInMsec };
        _timer.Tick += (_s, _e) => 
                {
                    _timer.Stop();
                    if (_connectThread.ThreadState == ThreadState.Running)
                        _connectThread.Abort();
                };
        };
    }

    private void Connected()
    {

    }

    private void Aborted()
    {

    }

    private void Connect()
    {
        try
        {
            DoConnect3rdPartyStuff();
            this.Invoke(Connected);
        }
        catch(ThreadAbortException)
        {
            // aborted
            this.Invoke(Aborted);
        }       
    }   
}
Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • I'm not in a `Form` but rather in a presenter coded against a view interface, so don't have access to the form's `Invoke` method – user5877732 Mar 03 '17 at 10:15
  • I used the Invoke to synchronize the call, i'm not familiar with `view interface`. You might use something that is equivalent. – Jeroen van Langen Mar 03 '17 at 10:22
  • It simply means that I don't have a reference to an object of type `Windows.Forms.Form` but rather an interface to a generic "view" which exposes only the necessary parts to the presenter so as to decouple the application from the technology (WinForms in this case). I was hoping for something a bit more elegant which wouldn't require every view to expose an `Invoke` method... – user5877732 Mar 03 '17 at 10:38
  • You could solve the synchronization within the `Connected()` method instead of the thread method. The synchronization is only needed when touching the controls etc. – Jeroen van Langen Mar 03 '17 at 11:10
  • Not without a way to get the UI thread in `Connected`, no. The `BackgroundWorker.RunCompleted` did exactly that for me, namely being able to run the callback on the calling thread (the UI thread) – user5877732 Mar 03 '17 at 11:44
  • The BackgroundWorker uses the SynchronizationContex, so uses the same technique as Invoke. So one way or another. You could use a queue and a timer to synchronize, but thats ugly. – Jeroen van Langen Mar 03 '17 at 11:58