Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Threading - How to terminate a work/background thread with UI interaction

Compact Framework, Windows Mobile 6, C#.

I'm having a go at some background threading on the compact framework, and have a question re: terminating the worker thread.

The Code

I have the following ThreadWorker class (code from here), which when executed, will perform a check at certain points to see if it should quit or not.....

public class ThreadWorker 
{

    public event EventHandler<ProgressEventArgs> OnProgress;

    protected virtual void Progress(ProgressEventArgs args)
    {
        if (OnProgress != null)
            OnProgress(this, args);
    }

    private void DoLongProcess()
    {
        // This will take a long time.
        Thread.Sleep(15000);
        Progress(new ProgressEventArgs("Some info for the UI to display."));
        Thread.Sleep(15000);
    }

    public void DoSomeBackgroundWork()
    {
        try
        {
            while (!Stopping)
            {
                DoLongProcess();
                if (Stopping) return;

                DoLongProcess();
                if (Stopping) return;

                DoLongProcess();
                if (Stopping) return;

                DoLongProcess();
                if (Stopping) return;
            }
        }
        finally
        {
            SetStopped();
        }

        Console.WriteLine("DoSomeBackgroundWork: Terminating gracefully.");
    }

    /// <summary>
    /// Lock covering stopping and stopped
    /// </summary>
    readonly object locker = new object();

    /// <summary>
    /// Whether or not the worker thread has been asked to stop
    /// </summary>
    bool stopping = false;

    /// <summary>
    /// Whether or not the worker thread has stopped
    /// </summary>
    bool stopped = false;

    /// <summary>
    /// Returns whether the worker thread has been asked to stop.
    /// This continues to return true even after the thread has stopped.
    /// </summary>
    public bool Stopping
    {
        get
        {
            lock (locker)
            {
                return stopping;
            }
        }
    }

    /// <summary>
    /// Returns whether the worker thread has stopped.
    /// </summary>
    public bool Stopped
    {
        get
        {
            lock (locker)
            {
                return stopped;
            }
        }
    }

    /// <summary>
    /// Tells the worker thread to stop, typically after completing its 
    /// current work item. (The thread is *not* guaranteed to have stopped
    /// by the time this method returns.)
    /// </summary>
    public void Stop()
    {
        lock (locker)
        {
            stopping = true;
        }
    }

    /// <summary>
    /// Called by the worker thread to indicate when it has stopped.
    /// </summary>
    void SetStopped()
    {
        lock (locker)
        {
            stopped = true;
        }
    }
}

...and the following form initiates the thread...

public partial class Test : Form
{
    public Test()
    {
        InitializeComponent();
    }

    private ThreadWorker myThreadWorker;
    private Thread t = null;

    private void Test_Load(object sender, EventArgs e)
    {
        myThreadWorker = new ThreadWorker();

        myThreadWorker.OnProgress += new EventHandler<ProgressEventArgs>(myThreadWorker_OnProgress);
    }

    private void miStart_Click(object sender, EventArgs e)
    {
        try
        {
            listResults.Items.Insert(0, "Set-up Thread.");
            t = new Thread(myThreadWorker.DoSomeBackgroundWork);
            t.Name = "My Thread";
            t.Priority = ThreadPriority.BelowNormal;
            t.Start();

            listResults.Items.Insert(0, "Thread started.");

        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    private void miStop_Click(object sender, EventArgs e)
    {
        try
        {
            listResults.Items.Insert(0, "Waiting for My Thread to terminate.");
            listResults.Refresh();
            myThreadWorker.Stop();
            t.Join();
            listResults.Items.Insert(0, "My Thread Finished.");
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }

    private delegate void InsertToListBoxDelegate(String text);
    private void InsertToListBox(String text)
    {
        if (InvokeRequired)
            Invoke(new InsertToListBoxDelegate(InsertToListBox), new object[] { text });
        else
        {
            listResults.Items.Insert(0, "{0}".With(text));
            listResults.Refresh();
        }
    }

    void myThreadWorker_OnProgress(object sender, ProgressEventArgs e)
    {
        InsertToListBox(e.Text);
    }
}

The Problem

When I click on the Stop button it calls...

myThreadWorker.Stop();
t.Join();
listResults.Items.Insert(0, "My Thread Finished.");

...what I was expecting is for the ThreadWorker to carry on with its current DoLongProcess() until it had finished, and still raise event to the UI via the OnProgress event handler and myThreadWorker_OnProgress.

However, what actually happens is when OnProgress is raised, the application freezes on the line reading...

 Invoke(new InsertToListBoxDelegate(InsertToListBox), new object[] { text });

The question

How do I call...

myThreadWorker.Stop();
t.Join();

...and still respond to events from the background thread until it is terminated?

like image 393
ETFairfax Avatar asked Feb 06 '26 08:02

ETFairfax


1 Answers

By calling Thread.Join you have blocked the UI thread. By calling Control.Invoke you have blocked the worker thread. Invoke posts a message to the UI thread's message queue and waits for it to be processed. But, since the UI thread is blocked waiting for the worker thread to complete it cannot begin executing the delegate which turn forces the worker thread to stall. The threads are now deadlocked.

The biggest problem is the Join call. It would be better to avoid calling Join from the UI thread. Instead, disable the stop button after it is clicked to provide feedback to the user that the stop request was accepted and is pending. You might even want to display a simple message on the status bar stating as much to make it clear. Then when the last OnProgress event is raised that will be your signal that the thread terminated and you can reset everything on the form.

However, you might want to consider a radical shift in thought. I think the Control.Invoke methodology is way overused. Instead of using Control.Invoke to marshal the execution of the event handler back onto the UI thread you could have the UI thread poll for the progress information using a timer. When new progress information is available the worker will publish it to some variable or data structure. This has several advantages.

  • It breaks the tight coupling between the UI and worker threads that Control.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).
like image 195
Brian Gideon Avatar answered Feb 07 '26 22:02

Brian Gideon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!