Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Closing side thread before closing the main form

I'm making a GUI application with windows forms in Visual Studio .NET 2010 using C#. I have multiple threads, for example:

  1. Thread that is pinging target device to make sure the connection is not lost
  2. A thread that requests and receives measurement results from the device
  3. A thread that is constantly updating the main form with these results. This thread invokes changes in main form controls using Control.Invoke(). My goal: I want to make sure that form is closed properly when user presses (x) on the main form. That means I want my connection to close, all side threads to terminate etc. I'm trying to use FormClosing event. There I set KillThreads flag to true and wait for side threads to terminate. There is a check in each, something like
if(KillThreads)
    return;

But there is an issue. If I try to wait for all threads to be terminated

for(;;)
if(ActiveSideThreads == 0)
    break;
closeConnection();

the thread N3 freezes blocked by Invoke() call; Sure it is, because the main gui thread is in infinite loop. So the form closing is delayed forever.

If I implement before-close operations as a thread procedure 4, and make a new thread in FromClosing event handler, form closes immediately, before the thread 4 termination, and Invoke() throws an error - t. 4 could not close thread 3 in time. If I add Join(), Invoke() calls are blocked again, and thread 3 is never terminated, so thread 4 waits forever.

What should I do? Is there some way to resolve this issue?

like image 571
HtonS Avatar asked Sep 08 '11 07:09

HtonS


2 Answers

First off decide whether or not you really need to be using Invoke at all. It is, and has been for a while now, my opinion that Invoke is one of the most overused techniques for causing an action to occur on the UI thread after it has been initiated from a worker thread. An alternative is to create some type of message object in the worker thread describing the action that needs to be done on the UI thread and place it into a shared queue. The UI thread will then poll this queue on some interval using System.Windows.Forms.Timer and cause each action to occur. This has several advantages.

  • It breaks the tight coupling between the UI and worker threads that Invoke imposes.
  • It puts the responsibility of updating the UI thread on the UI thread where it should belong anyway.
  • The UI thread gets to dictate when and how often the update should take place.
  • There is no risk of the UI message pump being overrun as would be the case with the marshaling techniques initiated by the worker thread.
  • The worker thread does not have to wait for an acknowledgement that the update was performed before proceeding with its next steps (ie. you get more throughput on both the UI and worker threads).
  • It is a lot easier to handle shutdown operations since you can pretty much eliminate all race conditions that would normally be present when requesting worker threads to gracefully terminate.

Of course, Invoke is very useful and there are many reasons to keep using this approach. If you do decide to keep using Invoke then read more.

One idea is to set the KillThreads flags in the Form.Closing event and then cancel closing the form by setting FormClosingEventArgs.Cancel = true. You may want to let the user know that a shutdown has been requested and is in progress. You may want to disable some of the controls on the form so new actions cannot be initiated. So basically we are signaling that a shutdown has been requested, but are deferring the closing of the form until the worker threads have shutdown first. To do this you may want to start a timer that periodically checks to see if the worker threads have ended and if they have then you can call Form.Close.

public class YourForm : Form
{
  private Thread WorkerThread;
  private volatile bool KillThreads = false;

  private void YourForm_Closing(object sender, FormClosingEventArgs args)
  {
    // Do a fast check to see if the worker thread is still running.
    if (!WorkerThread.Join(0))
    {
      args.Cancel = true; // Cancel the shutdown of the form.
      KillThreads = true; // Signal worker thread that it should gracefully shutdown.
      var timer = new System.Timers.Timer();
      timer.AutoReset = false;
      timer.SynchronizingObject = this;
      timer.Interval = 1000;
      timer.Elapsed = 
        (sender, args) =>
        {
          // Do a fast check to see if the worker thread is still running.
          if (WorkerThread.Join(0)) 
          {
            // Reissue the form closing event.
            Close();
          }
          else
          {
            // Keep restarting the timer until the worker thread ends.
            timer.Start();
          }
        };
      timer.Start();
    }
  }    
}

The code above calls Join, but it specifies a timeout of 0 which causes the Join call to return immediately. This should keep the UI pumping messages.

like image 65
Brian Gideon Avatar answered Sep 23 '22 06:09

Brian Gideon


I would have a top level exception handler in your thread that's performing the Invoke, that swallows the exceptions arising from Invoke if it discovers KillThreads is true. Then all you need is for the main thread to set KillThreads to true and allow the message loop to end.

My general technique, rather than a simple boolean variable, would be to use a ManualResetEvent as the signal for other threads to abort. The main reason for doing this is if I have long-running non-UI threads that want to sleep sometimes. By using the event, I can replace calls to Thread.Sleep with calls to Event.WaitOne, which means the thread will always wake up immediately if it has to abort.


In either case though, I don't usually wait around for the threads to signal completion. I trust them to do so in a timely manner, and don't tend to end up with subsequent code that relies on the threads not running. If you do have such a circumstance, perhaps you could elaborate on what it is?

like image 44
Damien_The_Unbeliever Avatar answered Sep 22 '22 06:09

Damien_The_Unbeliever