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.
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 newin 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
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
SmartThreadPool from this CodeProject article. Among other things, this object provides the
two afore mentioned (missing) features, and is you can modify it 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.
I this object, we establish some control over the thread pool via properties:
/// <summary>
/// Get/set the thread pool
/// </summary>
public SmartThreadPool Pool { get; set; }
/// <summary>
/// Get/set the number of concurrent threads that can be running at once
/// </summary>
public int ConcurrentThreads { get; set; }
/// <summary>
/// Get/set the maximum number pf threads we will be running
/// </summary>
public int MaxThreads { get; set; }
/// <summary>
/// Get/set the default thread pool startup criteria
/// </summary>
public STPStartInfo StpStartInfo { get; set; }
/// <summary>
/// Get/set how long the thread pool is idle before it times out
/// </summary>
public int PoolIdleTimeout { get; set; }
/// <summary>
/// Get/set the progress reporting frequency to assign to all threads.
/// </summary>
public int ProgressFrequency { get; set; }
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.
public void Reset(int maxThreads, int concurrentThreads, int frequency)
{
Clear();
this.MaxThreads = maxThreads;
this.ConcurrentThreads = concurrentThreads;
this.ProgressFrequency = frequency;
for (int i = 1; i <= MaxThreads; i++)
{
ProcessThread process = new ProcessThread(i);
process.ProgressFrequency = this.ProgressFrequency;
Add(process);
}
if (this.Pool != null)
{
this.Pool.Cancel();
while (this.Pool.ActiveThreads != 0)
{
Thread.Sleep(50);
}
this.Pool.Dispose();
this.Pool = null;
}
this.Pool = new SmartThreadPool(this.StpStartInfo);
}
After the thread pool (and its potential content threads) has been created, and the user
clicks the Start button on the form, the Start method is called. As you can see, we don't
have much to do here:
public void Start()
{
if (this.Pool != null)
{
QueueWorkItems();
this.Pool.Start();
}
}
Queueing work items is equally unexciting (and as brief as the Start method), but it's a
necessary chore for your SmartThreadPool.
private void QueueWorkItems()
{
if (this.Pool != null)
{
foreach (ProcessThread thread in this)
{
IWorkItemResult workItem = thread.QueueProcess(this.Pool);
}
}
}
As you can see, there's not a whol lot going on in that class.
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:
DateTime now = DateTime.Now;
int seed = (((now.Second * 1000) + now.Millisecond) % 23) * id;
Random random = new Random(seed);
int step = 0;
int duration = 0;
do
{
int lastStep = step - 1;
duration = random.Next(500, 10000);
if ((lastStep < 0) ||
(duration < this.stepDurations[lastStep] - 500 ||
duration > this.stepDurations[lastStep] + 500))
{
this.stepDurations[step] = duration;
step++;
}
} while (step <= 2);
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/millsecond 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 un-ordered processing that occurs in the demo
application. I felt that this would prove that the threads are indeed starting,
progressing, and finsihing 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 queueing itself into the specified thread pool:
public IWorkItemResult QueueProcess(SmartThreadPool pool)
{
workItemResult = null;
if (pool != null)
{
if (threadPoolState == null)
{
threadPoolState = new object();
}
workItemResult = pool.QueueWorkItem(new WorkItemCallback(this.Start),
WorkItemPriority.Normal);
}
return workItemResult;
}
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.
public object Start(object state)
{
started = DateTime.Now;
AdvanceStep(0);
AdvanceStep(1);
AdvanceStep(2);
finished = DateTime.Now;
RaiseEventProcessComplete();
return state;
}
As mentioned before, the ProcessThread object proceeds through three distict 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).
private void AdvanceStep(int step)
{
// advance the current step indicator
this.Step++;
Debug.WriteLine(string.Format("{0:00000} - Performing Step {1}", ProcessID, this.Step));
// set the progress for this step to 0
Progress = 0;
// post an event announcing that we're starting processing for the next step
PostStepChangeEvent(Step);
// Calculate our sleep interval between 1 percent progress reports.
int stepDuration = stepDurations[step];
int span = 0;
int interval = (int)(Math.Ceiling((double)stepDuration / 100d));
do
{
Thread.Sleep(interval);
span += interval;
this.Progress++;
if (this.ProgressFrequency > 0 && this.Progress % this.ProgressFrequency == 0 || this.Progress == 100)
{
RaiseEventProcessProgress();
}
} while (span < stepDuration);
// make sure we report 100% progress
if (Progress < 100)
{
Progress = 100;
RaiseEventProcessProgress();
}
// we do this to allow the 100% complete to be displayed (and visually
// digested by the user) on the progress bar.
Thread.Sleep(interval);
}
After I wrote the original code, I decided that it might be desireable (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 eents would be posted.
So, I provided an alternative interval sleep control loop if you wanted to play around with
that:
int interval = (int)(Math.Ceiling((double)stepDuration / 100d)) * this.ProgressFrequency;
do
{
Thread.Sleep(interval);
span += interval;
if (this.ProgressFrequency > 0)
{
this.Progress += ProgressFrequency;
RaiseEventProcessProgress();
}
} while (span < stepDuration);
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 progressbar 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 ite3ms 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:
public override string ToString()
{
return string.Format("{0:00000}", ProcessID);
}
Up to this point, we've establish the the core framework for the application. Now, it's time
to get dirty, so roll up your sleeves and get ready to dig in.
Custom Events
Because we wat 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 custome 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.
I started by defining the events in the ProcessThread object:
public event ProcessStep1Handler ProcessStep1 = delegate{};
public event ProcessStep2Handler ProcessStep2 = delegate{};
public event ProcessStep3Handler ProcessStep3 = delegate{};
public event ProcessCompleteHandler ProcessComplete = delegate{};
public event ProcessProgressHandler ProcessProgress = delegate{};
Next, I defined the event raising methods. Since they're all pretty much identical, I'm only
showing one of them here:
protected void RaiseEventProcessStep1()
{
ProcessStep1(this, new ProcessThreadEventArgs());
}
I suppose I could have been a lot more elegant about it, but what the heck, this is only a
demo app, and I'm not currently empoyed, soo I had plenty of time for typing. :)
Next, I had to add some code to the application form, because that's where we'd be
consuming the events.
The Form Event Handlers
The first thing that's needed are delegate definisiotns for the
methods that will be used during the Invoke calls (when a ProcessTHread object posts an
event).
private delegate void DelegateUpdateListboxStep1(ProcessThread thread);
private delegate void DelegateUpdateListboxStep2(ProcessThread thread);
private delegate void DelegateUpdateListboxStep3(ProcessThread thread);
private delegate void DelegateUpdateListboxComplete(ProcessThread thread);
private delegate void DelegateUpdateProgress(ProcessThread thread);
As you can see, we have an event handler delegate for every event. These definitions
allow us to specify a form method that can be used to interact with the UI. Those methods
look something like this:
//--------------------------------------------------------------------------------
private void UpdateListboxStep1(ProcessThread thread)
{
RemoveFromOtherListboxes(this.listboxStep1, thread);
}
//--------------------------------------------------------------------------------
private void UpdateListboxStep2(ProcessThread thread)
{
// clear the step 1 progressbar if necessary
if (this.listboxStep1.SelectedItem == thread)
{
this.progressStep1.Value = 0;
}
RemoveFromOtherListboxes(this.listboxStep2, thread);
}
//--------------------------------------------------------------------------------
private void UpdateListboxStep3(ProcessThread thread)
{
// clear the step 2 progressbar if necessary
if (this.listboxStep2.SelectedItem == thread)
{
this.progressStep2.Value = 0;
}
RemoveFromOtherListboxes(this.listboxStep3, thread);
}
//--------------------------------------------------------------------------------
private void UpdateListboxComplete(ProcessThread thread)
{
// clear the step 3 progressbar if necessary
if (this.listboxStep3.SelectedItem == thread)
{
this.progressStep3.Value = 0;
}
RemoveFromOtherListboxes(this.listboxComplete, thread);
RemoveEventHandlers(thread);
}
You may have noticed that I took the opportunity to unattached 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.
//--------------------------------------------------------------------------------
private void UpdateProgressBars(ProcessThread thread)
{
switch (thread.Step)
{
case 1 :
if (this.listboxStep1.Items.Count > 0 &&
this.listboxStep1.SelectedItem == thread)
{
this.progressStep1.Value = thread.Progress;
}
break;
case 2 :
if (this.listboxStep2.Items.Count > 0 &&
this.listboxStep2.SelectedItem == thread)
{
this.progressStep2.Value = thread.Progress;
}
break;
case 3 :
if (this.listboxStep3.Items.Count > 0 &&
this.listboxStep3.SelectedItem == thread)
{
this.progressStep3.Value = thread.Progress;
}
break;
}
}
As you can see, each event does something justy a little different with regards to the
UI.
Somewhere in the form code, we need to attach to the event handlers in the ProcessThread
objects, like so (the form architachture will be discussed a little later):
for (int i = 0; i < this.processManager.Count; i++)
{
this.processManager[i].ProcessStep1 += new ProcessStep1Handler(Form1_ProcessStep1);
this.processManager[i].ProcessStep2 += new ProcessStep2Handler(Form1_ProcessStep2);
this.processManager[i].ProcessStep3 += new ProcessStep3Handler(Form1_ProcessStep3);
this.processManager[i].ProcessProgress += new ProcessProgressHandler(Form1_ProcessProgress);
this.processManager[i].ProcessComplete += new ProcessCompleteHandler(Form1_ProcessComplete);
}
Finally, we need to add meat to the event handlers. They're all pretty much the same, so
I'll only show one of them here:
void Form1_ProcessComplete(object sender, ProcessThreadEventArgs e)
{
if (this.listboxComplete.InvokeRequired)
{
DelegateUpdateListboxComplete method = new DelegateUpdateListboxComplete(UpdateListboxComplete);
Invoke(method, sender);
}
else
{
UpdateListboxComplete(sender as ProcessThread);
}
}
In the case of our demo application, the listboxes are the only way these events get
posted, so having the if InvokeRequired line is kinda pointless. 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 delagte
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.
private void buttonLoadQueue_Click(object sender, EventArgs e)
{
if (this.processWorker.IsBusy)
{
this.processWorker.CancelAsync();
}
if (!string.IsNullOrEmpty(this.textboxProcessCount.Text))
{
// if you want to play with it, change the last parameter in the following method call
this.processManager.Reset(Convert.ToInt32(this.textboxProcessCount.Text),
this.trackbarConcurrent.Value,
10);
// add the event handlers for all of the threads
for (int i = 0; i < this.processManager.Count; i++)
{
this.processManager[i].ProcessStep1 += new ProcessStep1Handler(Form1_ProcessStep1);
this.processManager[i].ProcessStep2 += new ProcessStep2Handler(Form1_ProcessStep2);
this.processManager[i].ProcessStep3 += new ProcessStep3Handler(Form1_ProcessStep3);
this.processManager[i].ProcessProgress += new ProcessProgressHandler(Form1_ProcessProgress);
this.processManager[i].ProcessComplete += new ProcessCompleteHandler(Form1_ProcessComplete);
}
this.listboxQueued.Items.Clear();
this.listboxQueued.Items.AddRange(this.processManager.ToArray());
}
this.buttonStart.Enabled = true;
this.buttonLoadQueue.Enabled = false;
}
When you click the Start button, the button controls are appropriately enabled/disabled,
and the process manager starts the thread pool. This button cannot be clicked if the
thread pool is running.
private void buttonStart_Click(object sender, EventArgs e)
{
this.buttonStop.Enabled = true;
this.buttonClear.Enabled = false;
this.buttonStart.Enabled = false;
this.buttonLoadQueue.Enabled = false;
this.textboxProcessCount.Enabled = false;
this.processWorker.RunWorkerAsync();
}
When you click the Stop button, the threads that are running are cancelled, the thread
pool is stopped, the listboxes are cleared, and the buttons are appropriately
enabled/disabled. This button cannot be clicked unless the thread pool is running.
private void buttonStop_Click(object sender, EventArgs e)
{
StopThreadPool();
this.listboxComplete.Items.Clear();
this.listboxStep1.Items.Clear();
this.listboxStep2.Items.Clear();
this.listboxStep3.Items.Clear();
this.listboxQueued.Items.Clear();
this.listboxQueued.Items.AddRange(this.processManager.ToArray());
this.buttonStop.Enabled = false;
this.buttonClear.Enabled = true;
this.buttonStart.Enabled = true;
this.buttonLoadQueue.Enabled = true;
this.textboxProcessCount.Enabled = true;
}
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.
private void buttonClear_Click(object sender, EventArgs e)
{
this.listboxComplete.Items.Clear();
this.listboxStep1.Items.Clear();
this.listboxStep2.Items.Clear();
this.listboxStep3.Items.Clear();
this.listboxQueued.Items.Clear();
this.processManager.Clear();
this.buttonLoadQueue.Enabled = true;
this.buttonStart.Enabled = false;
this.buttonStop.Enabled = false;
}
When entering the number of threads to be processed, the user's input is verified to be
only numeric data. This is handled in the TextChanged event handler.
private void textboxProcessCount_TextChanged(object sender, EventArgs e)
{
if (!this.validatingText)
{
this.validatingText = true;
if (!string.IsNullOrEmpty(this.textboxProcessCount.Text))
{
int caretPos = this.textboxProcessCount.SelectionStart;
string text = this.textboxProcessCount.Text;
int value;
if (!int.TryParse(text, out value))
{
this.textboxProcessCount.Text = text.Substring(0, text.Length - 1);
this.textboxProcessCount.SelectionStart = text.Length - 1;
}
}
}
this.validatingText = false;
}
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:
//--------------------------------------------------------------------------------
void processWorker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
if (processManager.Count > 0)
{
this.processManager.Start();
while (!worker.CancellationPending && this.processManager.Pool.ActiveThreads > 0)
{
Thread.Sleep(100);
}
}
}
//--------------------------------------------------------------------------------
void processWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
this.buttonStart.Enabled = true;
this.buttonLoadQueue.Enabled = true;
this.textboxProcessCount.Enabled = true;
}
Essentially, the backgrodun 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.
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 of any
complexity requires this functionality. Add the power and flexinbility of
SmartThreadPool, and you can litterally do anything you can dream up.
I included the entire original SmartThreadPool ZIP file (there's a link to the absolute
most current version in the original article at http://www.codeproject.com/KB/threads/smartthreadpool.aspx