Multithreading is something that seems to keep popping up on me when I am coding.
My applications either need to do some crazy calculation (and not return 42) or
a function will be waiting for a while before returning anything. Both of these
leave my GUI locked up to the point where in Vista I get a straight black screen
and the wait O, and in XP if I click on the GUI I get yelled at for touching an
application that is not responding. I soon learned you can't just call all your
code out of the GUI, there are these sweet abstractions like…Business Logic Layer,
and Data Access layer. But that's not what this post is about. If I get my code
away from the GUI but it is still called by say, a button click, the process will
still not respond and the user will be unable to interact with the GUI or get any
feedback about execution.
The answer to this problem is fairly simple Multithreading! There are a few different
ways to go about it. The two I've used the most are what I will talk about,
BackgroundWorkers and
Threads.
System. ComponentModel.BackgroundWorker
BackgroundWorkers were the easiest way for me to get into multithreading an application,
and they are the first way I did. BackgroundWorkers take care of all the busy work
behind the scenes for you, you don't even have to know what a thread pool is, or
Mutex, or Semaphore, or even apartment states. There are some setbacks though. You can't build a Ferrari with legos.
To get your BackgroundWorker going first create a BackgroundWorker object, this
takes no arguments. There are a few events you will need to handle as well. DoWork
is called when the BackgroundWorker is started from RunWorkerAsync; DoWork is where
the meat of your processing intense code will go. If you need to pass an argument
to your thread you can use RunWorkerAsync(object), the drawback is you can only
send one object to the worker so you may need to write your own container class.
ProgressChanged is called when ReportProgress(int) is called, one drawback of this
is you can only represent completion on a 1-100 basis (progress bar) which for some
applications makes sense but there is no built in method of text feedback on progress.
And lastly you will need to handle RunWorkerCompleted which is called whenever the
worker ends. This could be because of an error in the thread, the thread dying naturally
or by calling CancelAsync.
BackgroundWorker bw = new
BackgroundWorker();
bw.DoWork += new
DoWorkEventHandler(bw_DoWork);
bw.ProgressChanged += new
ProgressChangedEventHandler(bw_ProgressChanged);
bw.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
In bw_DoWork any arguments passed to the worker are in
DoWorkEventArgs.Argument but this example
takes no arguments.
void bw_DoWork(object sender,
DoWorkEventArgs e)
{
e.Result = doMassiveWorkOMG();
return;
}
ProgreessChanged is simple enough. If I have some metric to determine what point
in finishing I am at I can update a progress bar to show the user that progress.
Inside the doMassiveWorkOMG function:
while(stillDoingEpicWork)
{
bw.ReportProgress(percentComplete);
}
Simple enough right? So now when doMassiveWorkOMG is done with its work bw_RunWorkerCompleted
will be called. Inside bw_RunWorkerCompleted we need to handle and errors that may
have occurred (if we care) and handle what happens at the end of worker execution.
void bw_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
throw new OMGWhyDidYouCancelMeException();
else if (e.Error !=
null)
throw e;
else
{
sendResultToSomeplaceAwesome(e.Result);
giveChildrenOfTheWorldCandy();
}
}
That's all there is to BackgroundWorkers, fairly simple and gets the job done. Now
lets do the same thing in a Thread.
System.Threading.Thread
Threads have a bit more going on, and as such are a little more dificult to use
but provides more. Creating threads is a little more dificult than a BackgroundWorker;
a Thread takes a ThreadStart object or a ParameterizedThreadStart delegate that
links to a void(void) or void(object) function respectivly. So to recreate our previous
example we would create our thread like this
Thread myThread = new
Thread(ThreadStart(doMassiveWorkOMG));
myThread.Name = "MassiveWorkThread";
I should mention, calling "new Thread()" is discouragedthe favored use is calling ThreadPool.QueueWorkItem.
The name only helps us while in the debugger we can see where the thread is running
in the Thread Window. There are plenty of other properties to set for the thread
such as Priority which sets the priority of the thread, IsBackground which sets
wheather the thread runs as a background process or not, and ApartmentState which
lets you set if the ApartmentState of the thread is STA or MTA.
Starting the thread is as simple as BackgroundWorker in that we simply need to call
myThread.Start()
which calls the function we put in our ThreadStart delegate, In this case
doMassiveWorkOMG. Keeping track of progress isnt as simple as a BackgroundWorker
but in much the same way we increment a progress bar we can update a status label.
public void updateWorkPercentage(int
progress)
{//update the progress bar to a new value
theBestProgressBar.Dispatcher.Invoke(DispatcherPriority.Normal,
new System.Windows.Forms.MethodInvoker(delegate()
{
theBestProgressBar.Value = progress;
}));
}
public void updateApplicationStatus(string message)
{//update a status label with a new message
amazingLabel.Dispatcher.Invoke(DispatcherPriority.Normal,
new System.Windows.Forms.MethodInvoker(delegate()
{
amazingLabel.Content = message;
}));
}
It is worthwhile to say this method can be used in a BackgroundWorker as well. Take
note that I didn't just call label.content or progressBar.value. Since this will
be called on a thread that isnt the GUI thread you will end up with a sweet "The
calling thread cannot access this object because a different thread owns it." error
if you try to call it dirrectly. For an explination of exactly why it must be done
this way look
here.
Since we don't have a RunWorkerCompleted function or even the ReportProgress function
in threads we have to modify the function we call (doMassiveWorkOMG) to handle updating
its status, reporting its results (to the GUI/another function/etc)
void doMassiveWorkOMG()
{
while(stillDoingEpicWork)
{
updateWorkPercentage(percentComplete);
updateApplicationStatus(currentStatusMessage);
epicWorkResult = doMoreEpicWork();
}
reportWorkResult(epicWorkResult);
}
There are plenty of ways to get values back from the thread which are easily googleable
and Im going to leave out here to keep the post under 90 pages.
When should I use what!?
Well, I cant tell you that, you know the scope of your application better than I
do. But I tend to use BackgroundWorkers when I just need a simple function running
in a thread. If I need the ability to pause/resume, access COM objects like the
clipboard I use a Thread with an STA Apartment State (BackgroundWorkers cant do
COM Interop that I have found).
In the end you can make a BackgroundWorker do almost anything you can make a Thread
do with a little work. But I would rather just use the BackgroundWorker as it is
and move to Threads if I need anything more.