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().
Get those property values (the string and the integer) from the background thread and pass them to progress.
Sure, that might be an option. But isn't that walking around the problem rather than dealing with it ? Besides, it's only the integer property having the problem, not the string property (which happens to be the first in the list).
A similar issue further on : The other parameter is a List of PrintSystemJobInfo objects. Out of each element, only PrintSystemJobInfo.Name can be accessed, all others give the exception. It's starting to look like only the first property of any class object can be used.
If either of you is interested in checking it out I can isolate the code to demonstrate it.
 
Here is the complete code showing the problem. pq.Name is ok, pq.NumberOfJobs is off-limits !!??
The form should be showing the name of your default printer, and the current number of documents in the print queue. Probably zero, but you should see this briefly changing to 1 when you print something.

BackgroundWorker problem:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using System.Threading;
using System.ComponentModel;
using System.Printing;

namespace frut
{
    class PrintMonitor : Form
    {
        Label lblPrinter;
        Label lblDocuments;

        PrintMonitor()
        {
            Text = "PrintMonitor";
            Size = new Size(400, 200);
            lblPrinter   = new Label() {Text="Printer", Location=new Point(20, 20), Width=300 };
            lblDocuments = new Label() {Text="numDocs", Location=new Point(20, 60), Width=300 };
            Controls.Add(lblPrinter);
            Controls.Add(lblDocuments);

            var backgroundWorker = new BackgroundWorker() { WorkerReportsProgress = true };
            backgroundWorker.DoWork += BackgroundWorker_DoWork;
            backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
            backgroundWorker.RunWorkerCompleted += (oo, ee) => Close();
            backgroundWorker.RunWorkerAsync();
        }

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

        private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            var prms = (object[])e.UserState;
            var pq = (PrintQueue)prms[0];
            var jobs = (List<PrintSystemJobInfo>)prms[1];

            lblPrinter.Text    = pq.Name;
            //lblDocuments.Text  = $"Documents in queue : {pq.NumberOfJobs}";     // Gives exception !
            lblDocuments.Text  = $"Documents in queue : {jobs.Count}";            // Works !
        }

        private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            PrintQueue pq;
            List<PrintSystemJobInfo> jobs;

            var backgroundWorker = (BackgroundWorker)sender;

            while ( true )
            {
                pq = LocalPrintServer.GetDefaultPrintQueue();
                jobs = new List<PrintSystemJobInfo>(pq.GetPrintJobInfoCollection());
                backgroundWorker.ReportProgress(0, new object[] {pq, jobs});
                Thread.Sleep(1000);
            }
        }
    }
}
 
When I used to work in the Windows Printing team around the Win7 timeframe, we used to document that the handles to various print objects has thread affinities, and so you shouldn't be using the handle across threads -- each thread needed to open it's own handle. That was years ago for me. I don't know if any of the .NET Framework team or technical writers doing the documentation has made note to surface those requirements; or if they are just depending on the typical WinForms programmer only uses the UI thread and if they don't they'll get a nice exception stating the issue.
 
In the blog below, Raymond writes about printer DCs, but other printing related objects also has thread affinity as I stated above.

 
*sigh* I don't understand why there is even any doubt. There is really some thread affinity to the object. You should have read the exception error message:
System.InvalidOperationException
HResult=0x80131509
Message=The calling thread cannot access this object because a different thread owns it.
Source=WindowsBase

Here's the minimal repro:
C#:
using System;
using System.Printing;
using System.Threading;

class Program
{
    static void DumpPrintQueueDetails(PrintQueue pq)
    {
        Console.WriteLine(pq.Name);
        Console.WriteLine(pq.NumberOfJobs);
    }

    static void OtherThread(object obj)
    {
        DumpPrintQueueDetails((PrintQueue)obj);
    }

    static void Main()
    {
        var pq = LocalPrintServer.GetDefaultPrintQueue();
        DumpPrintQueueDetails(pq);
        var thread = new Thread(OtherThread);
        thread.Start(pq);
        thread.Join();
        Console.ReadKey();
    }
}

And change line 15 to:
C#:
DumpPrintQueueDetails(LocalPrintServer.GetDefaultPrintQueue());
and it works.
 
Oh, I did read the exception message and tried to make sense of it. And I would have done, probably, were it not for the fact that some properties work and some don't. I would never have expected thread affinity to be that granular 😮
Intuitively, my intention was do as much code as possible in DoWork, and mostly restrict the code in ProgressChanged to the UI updates. But that's proven to be unfeasible. So now I have moved nearly all the code to ProgressChanged, and everything works ! In fact DoWork is reduced to a mere husk, doing nothing more than leaving everything to ProgressChanged. Somehow this seems a bit counter-intuitive to me, but it seems to be the way to do it.

DoWork:
 while ( true )
 {
     ((BackgroundWorker)sender).ReportProgress(0, null);
     Thread.Sleep(1000);
 }

Anyway this solution is much cleaner than the original one with the Invokes and the Windows Timer. So now this topic is truly (and satisfactorily) resolved. Thanks for sticking with me !
 
It would be, were it not for the fact that the UI needs to remain responsive.
Um, no. If you call Invoke to marshal a method call to the UI thread then that method will monopolise the UI thread. If you're talking about the Thread.Sleep call then that's what a Timer is for. Your multithreading is completely and utterly pointless.
 
Um, no. If you call Invoke to marshal a method call to the UI thread then that method will monopolise the UI thread. If you're talking about the Thread.Sleep call then that's what a Timer is for. Your multithreading is completely and utterly pointless.
Hmm, that is rather discouraging to hear... just as I've got it all working smoothly with the expert help of the forum staff 😯
I don't hog the UI thread for more than I absolutely need to, but I was forced to do it more than I wanted to because of the thread affinity of the printer objects. All I intended to was updating the UI.

So are you saying that a worker thread is totally useless when you need to update the UI from the background ? Perhaps in hindsight, a timer would have been a better choice, but that idea didn't occur to me when I started. I might still try it, anything that uncomplicates things is welcome. OTOH, it looks like it's working fine now, so why change it...
 
So are you saying that a worker thread is totally useless when you need to update the UI from the background ?
Yes. The point of a worker thread is to do work that is not UI related. Any UI is just there to provide feedback to the user so that they are not left wondering what is happening.
 
Yes. The point of a worker thread is to do work that is not UI related. Any UI is just there to provide feedback to the user so that they are not left wondering what is happening.
Ok. So if I need a form that is both responsive and gets updated every second or so, what is the mechanism of choice ? Short of using a MVC model, which I understand will work for a DataGridView, but I don't see how it would work for things like labels or images that need to be updated ? Is a timer the proper solution ? You said there are 7 different timers available, what would be the best choice ?
 
Everything is doable with the right amount of abstraction.
 
Is a timer the proper solution ? You said there are 7 different timers available, what would be the best choice ?
If the timer tick event handler's job is to update the UI from the data model, then the Windows Timer is the best choice because everything is done on the UI thread.

Most of the other timers either make use of at thread pool, or are platform specific only for web servers.
 
Here's another variation of post #15 (another thread calling Invoke() to update UI), and post #21 (BackgroundWorker.ProgressChanged to update UI). This time it uses Progress<T> to update the UI. Work is still done on another thread by virtue of the Task.Run(), but the UI update is done on the UI thread via the magic of Progress<T> which knows which thread it was created on.

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

namespace WinFormsCore
{
    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);

            IProgress<int> progress = new Progress<int>(n => _lblCountdown.Text = $"{n}: {DateTime.Now}");

            Task.Run(() =>
            {
                for (int i = 10; i >= 0; i--)
                {
                    Thread.Sleep(1000);
                    progress.Report(i);
                }
            });

            CenterToParent();
        }
    }
}

You simply need to just declare enough [ICODE}Progress<T>[/icode]'s for all the pieces of UI that you need to update from the worker thread.

You still have the problem of trying to read from the UI within your worker thread. In general this problem looks to be self induced because you are using the UI as your data store. As an aside, there is no need to go full MVC or MVP. You just need to separate the data from your UI, and have a helper method to make the UI reflect your data. Of course, when you have that helper method, then updating the UI to report progress becomes trivial by simply calling that method (in the appropriate thread). Obviously binding will make things easier because you don't need to write that helper method, but trying to do binding in WinForms, although doable, is not always as easy as binding in WPF.
 
Last edited:
Back
Top Bottom