Resolved Detecting the end of a thread

cbreemer

Well-known member
Joined
Dec 1, 2021
Messages
184
Programming Experience
10+
I am struggling with something that I feel should be simple... once you know how.
I have a Windows Form that implements a rudimentary print queue monitor. It starts a thread which goes into an endless loop, each second checking the local printer queue and calling Invoke(new MethodInvoker(delegate ()... to update the UI. It works beautifully, but now my extra requirement is that as soon the number of jobs reaches zero, the form terminates itself. A fairly normal thing to want, I would hope ?

Trying to close the form from within the delegate function in the thread body does not seem like a good idea. No matter what I try, this hangs the application.
I guess it would have to be the main thread terminating the form. But how does the main thread know that the worker thread has ended ? I don't want to be polling the thread's isAlive flag as that would make the form unresponsive. I read about Thread.Abort() being unreliable, and I would not know how the main thread could catch the ThreadAbortException anyway. Is there some event that the thread can raise which then calls an event handler in the form ? I googled around but the dozens of different ideas and suggestions only serve to confound me 😯 I hope someone here has been there and done that.
 
Solution
On your UI thread, create a Windows timer that fires every second or so. In the handler far that timer, use Thread.Join(int) passing in a small delay -- perhaps just 100 milliseconds. Check the return value. If the return value is true, then the thread has terminated and you can call Form.Close().
On your UI thread, create a Windows timer that fires every second or so. In the handler far that timer, use Thread.Join(int) passing in a small delay -- perhaps just 100 milliseconds. Check the return value. If the return value is true, then the thread has terminated and you can call Form.Close().
 
Solution
That was the polling method. The officially correct way is to use a manual reset event. As the worker thread is terminating, have it set the event. On the UI thread, you need to wait for that event.

But then comes the rub. You can't wait for that event without locking up the UI thread unless you start playing games with async/await or once again polling, but this time polling the event state instead of polling the thread state.
 
And in the end, if you are just going to poll anyway, then there is no need to be fancy polling for thread or the event. You could simply poll a boolean flag that your thread sets to true when it is about to terminate. Often people will understand that kind of code better, than if they suddenly run across an unfamiliar Thread.Join() or WaitHandle.WaitOne() call.
 
Thanks for the ideas ! I had allbut dismissed polling as a Bad Idea... wanting neither to lock up the UI or introduce another thread. I had not thought of a Windows timer - does not not cause another thread too ? What about passing a callback function as the argument to Thread.Start, so that the thread can call that function when decodes it is time to close the form ? Not sure this would work, even if I could get the syntax right (damn, this stuff is complicated..)
 
Application.Exit seems to work fine from both background and foreground thread (your printer polling thread), don't even need to make sure the loop ends if it's a foreground thread.
 
Application.Exit seems to work fine from both background and foreground thread (your printer polling thread), don't even need to make sure the loop ends if it's a foreground thread.
I'm sure that will work, thanks, but I just want the monitor form (that was started by the application's main form) to exit 🙂
 
I see, then you can invoke a call to Close and break the loop from the printer polling thread.
 
On your UI thread, create a Windows timer that fires every second or so. In the handler far that timer, use Thread.Join(int) passing in a small delay -- perhaps just 100 milliseconds. Check the return value. If the return value is true, then the thread has terminated and you can call Form.Close().
I went with this solution and it seems to work ! It may not be the most elegant or most correct solution, but it is simple and I like that 😀
It surprises me a bit that the timer handler is allowed to call Form.Close(). I had thought the timer would run in another thread, and I'd have to invoke a delegate again. Very nice when things are simple for a change 😁
 
.NET now has 7 different kinds of timers. The Windows timer is based on the WM_TIMER message sent to the Windows message queue, therefore the handling of that message happens on the default UI thread.
 
Yes ! Skydiver's first suggestion works for me. But thanks for your input also.
But why complicate by adding timer and polling outside the thread? Your loop knows when it's done:
C#:
while (true)
{
    // poll printer
    if (jobs == 0)
        break;   
}
Invoke(() => Close());
 
.NET now has 7 different kinds of timers. The Windows timer is based on the WM_TIMER message sent to the Windows message queue, therefore the handling of that message happens on the default UI thread.
Aaahhhh.... honest-to-good old Windows stuff ! Much as I like .NET and C#, I have a feeling that it's getting too clever and unwieldy for its own good. Just like I felt about Java a couple of years ago.
Anyway thanks again for your tip which saved me a lot of headaches 👍
 
But why complicate by adding timer and polling outside the thread? Your loop knows when it's done:
C#:
while (true)
{
    // poll printer
    if (jobs == 0)
        break;  
}
Invoke(() => Close());
Yeah, something like this was what I had tried at first. But as I wrote, it totally hung the application. I'm afraid I could not be bothered to find out why 🙄
 
+1 to @JohnH 's point about just letting the other thread close the form. Here's some quick and dirty code that shows the feasibility of doing it.

C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Drawing;
using System.Threading.Tasks;
using System.Threading;
using System.ComponentModel;

namespace WinForms
{
    class MainForm : Form
    {
        MainForm()
        {
            Text = "Main";
            Size = new Size(800, 600);

            var button = new Button() { Text = "Start Countdown" };
            button.Click += (o, e) => { new OtherForm().Show(this); };
            Controls.Add(button);

            CenterToScreen();
        }

        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
    }

    class OtherForm : Form
    {
        Label _lblCountdown = new Label() { Text = "Starting...", Dock = DockStyle.Fill };

        public OtherForm()
        {
            Text = "Other";
            Size = new Size(400, 300);
            Controls.Add(_lblCountdown);
            Shown += (o, e) => new Thread(Countdown).Start();
            CenterToParent();
        }

        void Countdown()
        {
            for (int i = 10; i >= 0; i--)
            {
                Thread.Sleep(1000);
                _lblCountdown.Invoke((MethodInvoker) delegate
                {
                    _lblCountdown.Text = $"{i}: {DateTime.Now}";
                });
            }
            Invoke((MethodInvoker) delegate { Close(); });
        }
    }
}
 
Back
Top Bottom