Click here to Skip to main content
15,880,427 members
Articles / Programming Languages / C#

Multithreading, Delegates, and Custom Events

Rate me:
Please Sign up or sign in to vote.
4.95/5 (74 votes)
17 Mar 2010CPOL12 min read 189.1K   3.3K   310  
Tie it all together and not lose your mind in the process
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

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Paddedwall Software
United States United States
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.

Comments and Discussions