Introduction

At my last job, I was tasked with writing an application that was capable of processing hundreds of customers using multi-threading. This application was also supposed to perform other tasks based on the status of that processing. All the while, it needed to notify the user of each item's progress. The implementation included a series of list boxes that showed where each customer was in the process (better than having a bunch of progress bars on the screen). The demo app presented here parrots that design so that you can see how the various threads post and custom events in order to illustrate UI updating from one or more threads.

Note: In the interest of brevity, I omitted debugging and exception handling from the code snippets posted in this article.

General

The C# forum here (and more recently, Quick Answers) is routinely peppered with questions about updating UI components from threads other than the main UI thread. Indeed, this is a common requirement for the simplest of DotNet applications. This article won't really be showing you anything new in this regard, but it does illustrate some organization considerations, and exercises the UI update problem, all in an application that might closely resemble something you'd need to write in the "real world".

This article implements the following elements:

Multi-threading

Use of a thread pool

Using Invoke to update the UI

Custom events

Random number generation

Liquid Nitrogen

The Thread Pool

As you may already know, DotNet already provides a thread pool object. However, its usability is hampered by several shortcomings, such as you can only have one at a time in a given application, and you can't remove threads once they're queued. For this reason, I use Ami Bar's SmartThreadPool from this CodeProject article. Among other things, this object provides the two afore-mentioned (missing) features, and you can modify the source code yourself if it doesn't quite fit your needs. If you want details regarding the implementation and use of this class, you should go read that article.

The ProcessThreadManager Object

This object inherits from List, and contains the ProcessThread objects (described below). I did this because the ProcessThread items needed to be contained in a list, and because I wanted to abstract out the functionality required to get the thread pool prepped and started.

In this object, we establish some control over the thread pool via properties:

The Reset() method prepares the thread pool, and creates the specified number of process threads. Notice however that the thread pool remains idle because the user has to click a button to get things rolling.

The ProcessThread Object

The threads we use here are very simple, progressing through three sit-and-spin processing "steps". In other words, their general functionality is kind of pointless and almost completely useless in a real application. They exist merely to take some time to complete, but otherwise, perform no useful task.

The only slightly interesting part of all that is the method at which we arrive at each thread's processing time. Instead of just setting a hard-wired run-time for each cycle of each thread, I use a random value, which is calculated like so:

Each step has a duration. This duration is arrived at by seeding a Random object with a value based on the current time. Since the details are fairly obvious by looking at the code, the reasons aren't. I wanted to make sure the seed was sufficiently different from the seed used any of the other process threads, so I did some additional math on the second/millisecond combination to try to ensure that.

Once seeded, I executed a do/while loop that ensured that the random values were far enough apart to allow the user to see the unordered processing that occurs in the demo application. I felt that this would prove that the threads are indeed starting, progressing, and finishing at their own established intervals. The result is that it is highly improbably that big blocks of threads will progress and finish all at the same rate. In the end though, this really has nothing to do with what the demo application's true intent.

Part of using the SmartThreadPool is putting a thread into the queue in order for it to "managed" within the pool. Once queued, it becomes a "work item" within the pool. Threads are executed in the order they appear in the queue, and while this normally isn't an issue in a real-world app, it's handy to be aware of this if it matters in your application. The ProcessThread object contains a method for queuing itself into the specified thread pool:

Once queued, a thread can be started by the thread pool. In the snippet above, notice that we specify the callback method in this object as the thread start delegate. As you know, this is the thread proper, and contains all of the actual processing code for the thread.

As mentioned before, the ProcessThread object proceeds through three distinct steps while processing. The reason is so that we can notify the UI of each thread's progress as processing continues.

Note: You may have noticed the line that contains RaiseEventProcessComplete();. The ProcessThread object posts several events as it processes itself. Well talk about these events a little later in the article.

The AdvanceStep method uses the previously determined interval for the specified step to establish its processing duration. It divides the total duration by 100 to establish the sleep interval required for each 1% of completion. It then goes into a loop and sleeps for the establish interval, and raises a progress event at the end of each sleep period. The loop ends when the total duration meets or exceeds the interval that was calculated for the current step (in the constructor).

After I wrote the original code, I decided that it might be desirable (within the context of the demo application) to sleep longer before reporting progress. This would ease the load on the CPU because threads were sleeping longer and fewer progress events would be posted. So, I provided an alternative interval sleep control loop if you wanted to play around with that:

Keep in mind that this will have a direct effect on how long the thread processes, because of the last line in the method. In order to make sure the progress bar allows the user to see that a thread is complete, we sleep for the length of the interval after posting the 100% progress event. This means you're going to also have to change the last line of the method to divide the interval by the ProgressFrequency just to make sure you're not waiting too long for the thread to actually "complete".

Thread.Sleep((int)(interval / (double)this.ProgressFrequency));

In order for the items to be easily usable in the UI (when adding them to listboxes), I overrode the ToString() method to show the id number of the thread:

Up to this point, we've established the core framework for the application. Now, it's time to get dirty with the events, so roll up your sleeves and get ready to dig in.

Custom Events

Because we want to keep the user informed of the progress of the threads, the demo application makes heavy use of custom events and delegates. Because there is already sufficient reference material on the whys and wherefores of custom events and how they work, I'm not going to bother you with those kinds of details. Instead, I'll keep the discussion confined to the context of the demo application. Let's start with the ProcessThread object.

ProcessThread Events

The first thing I had to do was determine what kind of events I wanted to post, and came up with the following events:

Step advancement, allowing the UI to move a thread item from one list box to another

Processing progress, allowing the UI to reflect a selected thread's actual processing progress within a given "step".

Thread complete, so we can react to thread processing being completed.

You may have noticed that in the UpdateListboxComplete delegate method, I took the opportunity to unattach the event handlers for the finished thread. It's simply convenient to do it this way, and we can do it because we're theoretically done with the thread anyway.

Somewhere in the form code, we need to attach to the event handlers in the ProcessThread objects, like so (the form architecture will be discussed a little later):

In the case of our demo application, the list boxes and the progress bars are the intended update targets for these events, so having the if InvokeRequired line is kinda pointless. However, since we don't really know how this code might be modified in the future, we really should do it this way (although one of the event handlers doesn't check - it just assumes that Invoke is required).

You may have noticed that regardless of which way the control is updated, the same delegate method (UpdateListboxComplete) is used. That's because the actual delegate method doesn't care how it's called or where it's called from. It only matters to the UI thread, and that's why we use Invoke when updating from another thread.

The Demo Application

The demo is a simple WinForm application, comprised of a few basic controls. The user can specify the maximum number of threads to process (1 to 255), and how many threads to run concurrently (1 to 100). Beyond that, it's a simple matter of pushing buttons and watching it run.

When you click the Load Queue button, the process manager creates and initializes the pool, and creates the specified number of threads. This is also where the event handlers are attached for the threads.

When you click the Stop button, the threads that are running are canceled, the thread pool is stopped, the list boxes are cleared, and the buttons are appropriately enabled/disabled. This button cannot be clicked unless the thread pool is running.

When you click the Clear button, the list boxes are emptied out, the threads are deleted (the process manager has 0 items), and the buttons are appropriately enabled/disabled. This button cannot be clicked while the thread pool is running.

The code that positions the caret is somewhat lacking because I didn't feel like dealing with it. I make the admittedly lame (and lazy) assumption that the last character typed was at the end of the input field. If you're not in fact at the end of the input field and you type an invalid character, I suspect that the cursor will position itself at the end of the text as a result. I haven't actually tried it, but I leave it here for your general bemusement.

The final aspect of this demo app is that I monitor the status of the thread pool itself with a BackgroundWorker object. Like pretty much everything else in the demo, I didn't absolutely have to do it this way, but it was damn convenient, and I'm not one to go to great lengths to shirk a convenience. Here's the code:

Essentially, the background worker starts the process manager thread pool and sleeps for 100 milliseconds before seeing if the thread pool is idle (0 active threads). When complete, it enables/disables the appropriate buttons.

The Progress Bars

The progress events are only visible in the form if you click an item in one of the "Step" list boxes. At that point, you can watch the progress of the selected item. When the item progresses to the next listbox, the progress bar that was showing the progress is set to a value of 0, and the item's progress is no longer tracked in the subsequent listbox. It would an easy exercise for the programmer to allow the user to select an item in the Queued listbox and watch it's progress all the way through the processing cycle.

In Closing

The demo app exercises the thread pool and illustrates use of Invoke enabling non-UI threads to affect the UI. As stated before, virtually all applications if any complexity requires this functionality. Add the power and flexibility of SmartThreadPool, and you can literally do anything you can dream up.

History

03/17/2010 - Fixed some spelling errors.

02/14/2010 - An exception was being thrown in the demo application because the Stop button was enabled when it shouldn't have been. I also changed the demo app so that the Clear button was enabled when the thread processing was completed.

02/08/2010 - Updated to add text that got chopped off during initial posting. Also fixed some of the descriptive text, and rearranged some of the code snippets.

Share

About the Author

I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

In ProcessThread.cs in the function QueueProcess the line "workItemResult = pool.QueueWorkItem(new WorkItemCallback(this.Start), WorkItemPriority.Normal);" starts the thread function "Start()". How can you pass an object to Start()? When I try to pass it an object name, I get a compile error saying that a method name is required.

Ummm, you don't pass it there. What I think you might want to do is pass the object by setting a property in the class that contains the Start method, and work on that object from within the thread object. The object passed to the Start method is from SmartThreadPool and represents the thread state.

.45 ACP - because shooting twice is just silly-----"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997-----"The staggering layers of obscenity in your statement make it a work of art on so many levels." - J. Jystad, 2001

When I try to compile it spits this out, what does a noob need to do to get it to compile?

Error 1 The type or namespace name 'Amib' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThread.cs 10 7 MultiThreadDelegate
Error 2 The type or namespace name 'Amib' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThreadManager.cs 7 7 MultiThreadDelegate
Error 3 The type or namespace name 'IWorkItemResult' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThread.cs 19 11 MultiThreadDelegate
Error 4 The type or namespace name 'SmartThreadPool' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThread.cs 113 39 MultiThreadDelegate
Error 5 The type or namespace name 'IWorkItemResult' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThread.cs 113 10 MultiThreadDelegate
Error 6 The type or namespace name 'SmartThreadPool' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThreadManager.cs 17 10 MultiThreadDelegate
Error 7 The type or namespace name 'STPStartInfo' could not be found (are you missing a using directive or an assembly reference?) C:\Users\foo\Documents\Visual Studio 2008\Projects\MultiThreadDelegate\MultiThreadDelegate\ProcessThreadManager.cs 29 10 MultiThreadDelegate
Warning 8 Could not resolve this reference. Could not locate the assembly "SmartThreadPool". Check to make sure the assembly exists on disk. If this reference is required by your code, you may get compilation errors. MultiThreadDelegate

That's the SmartThhreadPool stuff, and if the DLL isn't included in the ZIP file (which I could have sworn it was), then there's a link to it in the article itself.

.45 ACP - because shooting twice is just silly-----"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997-----"The staggering layers of obscenity in your statement make it a work of art on so many levels." - J. Jystad, 2001

That's the SmartThreadPool stuff. I coulda sworn it was included in the zip file.

BTW, this is also a .Net 3.5 project.

.45 ACP - because shooting twice is just silly-----"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997-----"The staggering layers of obscenity in your statement make it a work of art on so many levels." - J. Jystad, 2001

Thanks. I really thought your XNA article was gonna run away with it (and it did in the best article of the month. ).

The funny thing is, I put more value on the reputation points than the available prizes.

.45 ACP - because shooting twice is just silly-----"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997-----"The staggering layers of obscenity in your statement make it a work of art on so many levels." - J. Jystad, 2001

I like your writing style; direct, no bs, like you're talking to another programmer.

Thanks. I assume that most people reading it will be programmers (alas, that isn't always the case ).

Hans Dietrich wrote:

Some things you might want to fix:

Done.

.45 ACP - because shooting twice is just silly-----"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997-----"The staggering layers of obscenity in your statement make it a work of art on so many levels." - J. Jystad, 2001

Well, the thread pool thing is simply a good idea if you have to manage possibly thousands of unique threads. The event handling stuff is merely to update the UI. In my case, I needed to move items from one list box to another. Other people may have wildly different requirements. As is the case with most of my articles, it's demo application is meant to illustrate the real-world problem I faced. The beauty of this code is that you can use all or part of it to equal effect, depending on your own requirements.

.45 ACP - because shooting twice is just silly-----"Why don't you tie a kerosene-soaked rag around your ankles so the ants won't climb up and eat your candy ass..." - Dale Earnhardt, 1997-----"The staggering layers of obscenity in your statement make it a work of art on so many levels." - J. Jystad, 2001