Click here to Skip to main content
15,881,844 members
Articles / Programming Languages / C#
Article

Step by Step Threads

Rate me:
Please Sign up or sign in to vote.
4.14/5 (8 votes)
31 May 2008CPOL8 min read 65K   515   37   28
Step by step cross thread communication and thread-safe form control updates.

Image 1

Introduction

This article tackles two problems. First, I have a long task I want to run in the background, but the user should be able to start using the Windows form immediately, even though the task is not done. I need the results of that background thread to be available for use in the original form. Second, I need the original form to know when the background thread is finished, and make changes to itself when that happens. I can do this with an event, but, the event handler in the original form can not update the form directly, because the .NET framework considers that event to be part of the background thread. I have to use BeginInvoke() in the original form to do GUI updates.

I am largely self taught, and learn a lot here on CodeProject. There are many articles on threading, but I didn't find any that gave a step by step process, direct and straight to the point. This article assumes that you know how to write a program in C#, have a clue as to what Threading is, and just want to know how to implement it in a C# Windows form.

I find the logic behind running a process in a background thread to be easier to grasp when the background thread is in a separate class, so I always implement it that way. If you need to set up your background thread in the same class as the calling thread, you can probably find another article that comes closer to showing what you need. [Update--See the Update section for an all in one class solution which I now like better.]

Background

In this demo project, I launch the background thread from a button click on the form to make it easy to see how things work together; in the real project which inspired this article, I launch it from Form.Load(). In the demo, I am filling a generic List<> with strings (and using Thread.Sleep(2000) to simulate a two second database query); in the real project, the List<> gets filled with business objects which are instantiated from a database query.

In the demo, the GUI update is just updating a label on the form; in the real project, I am holding the business objects in a generic List<> I can search (on the user's request), and the GUI update is adding a subset of those objects to the ListBox as initial suggestions. (Or, I might want to start with my Search button disabled, and enable it when the list is ready to search.) In the demo, I have a Join Thread button to demonstrate what happens if I try to use the list before the background thread has finished making it; in the real project, joining the thread is the first thing I do when the user attempts to search the list.

Running the demo

Run the program and click the "Start Thread" button. A label indicates that we have joined the thread. If you wait a couple of seconds, the label will indicate that the thread is done. When you click the "Join Thread" button, the label indicates that we are joining the thread.

If you click "Join Thread" before the thread is finished, the program will wait for the thread to finish, then the label will indicate that the thread is done and fill the ListBox. If you join the thread after it is done, the ListBox will fill, but the label will not change from "Joining Thread". I did this to demonstrate that we can join the thread whether it is finished or not. "Clear List" and "Join Thread" again to see that we can join the thread repeatedly.

Note: If you click "Start Thread", "Join Thread", and "Start Thread" again in rapid succession, the .NET framework will buffer the second "Start Thread" click, and execute it after the thread is finished. A method of avoiding this is explained below.

Using the code

Create a new Windows form, import System.Threading so we don't have to specify it all the time, and add a class for our background thread.

C#
using System.Threading;

public partial class MainThreadForm : Form
{
   //...Form code goes here...
}

public class BackgroundWorkThreadClass
{
   //...Worker class coge goes here...
}

Next, add the object which the form wants the background object to work on; in this example, a generic List<> of strings. Note that we construct a new List<> in the main form, but not in the background thread class. Our background class takes the already instantiated List<> as an argument to its constructor. Let's also add the work in the background thread class, DoLongTask(), and, in the form class, we'll instantiate the background class and its Thread and start the Thread.

Note that when we instantiate the Thread, we pass it a new ThreadStart, and we feed the ThreadStart bwt.DoLongTask and not bwt.DoLongTask(), as we would do if we were calling DoLongTask() directly. Also, remember that we would normally start the Thread in MainThreadForm_Load instead of a button click.

C#
public partial class MainThreadForm : Form
{
    private List<string> ResultList = new List<string>();
    private Thread bgWork;

    private void btnStart_Click(object sender, EventArgs e)
    {
    BackgroundWorkThreadClass bwt = 
            new BackgroundWorkThreadClass(ResultList);
    bgWork = new Thread(new ThreadStart(bwt.DoLongTask));
    bgWork.Start();
    lblStstus.Text = "Thread started";
    }
}

public class BackgroundWorkThreadClass
{
    private List<string> ResultList;

    public BackgroundWorkThreadClass(List<string> rList)
    {
        resultList = rList;
    }

    public void DoLongTask()
    {
        //Fill the list with data
        for (int x = 65; x <= 90; x++)
        {
            char c = (char)x;
            resultList.Add(c.ToString() + " = " 
                 + (x - 64).ToString());
        }
        //Simulate a 2 second long database query
        Thread.Sleep(2000);
    }
}

At this point, we have enough code to do the work. The button will launch the Thread, and when the Thread is done, our List<> back in the form will be populated with our strings. Now, we want to be able to use that List<>, but wait patiently if it isn't ready. Let's add code for the "Join Thread" button. Note that we want to do a GUI update before we join the Thread, but it won't run until the button click handler is done, and the handler won't finish until the thread does; so, we use Application.DoEvents() to force the GUI update immediately. (In a real project, we might want to do something like MainThreadForm.Enabled = false for our first line here, and then MainThreadForm.Enabled = true at the end of the handler to stop our impatient users from clicking everything in sight while they wait.)

C#
private void btnJoin_Click(object sender, EventArgs e)
{
    lblStstus.Text = "Joining Thread";
    Application.DoEvents();
    listResults.Items.Clear();
    bgWork.Join();
    listResults.Items.AddRange(ResultList.ToArray());
}

Now, we have done work in a background thread and used the results in the main thread safely, but we also want to automatically do some work in the GUI as soon as the task is done. We need to implement an event. This will require code in the form, the background work class, and in between the two. Between the form class and the worker class, add a delegate for the events and handlers to register with. We have to give the delegate EventArgs. We could also use a sender object, and we could use a custom EventArgs if we wanted to pass some information along with the event, but in this case, all we care about is being told when the work is done, so plain vanilla EventArgs will work just fine.

C#
public partial class MainThreadForm : Form
{
    ...Form code goes here...
}

public delegate void BgWorkDoneHandler(EventArgs e);

public class BackgroundWorkThreadClass
{
    ...Worker class coge goes here...
}

Now, the worker thread class needs an event. At the top of the class where we declared the private List<string> ResultList;, we will add this code:

C#
public event BgWorkDoneHandler OnWorkDone;

Then, the worker thread class needs to raise the event when the work is done, so we'll add this code to the DoLongTask() method.

C#
public void DoLongTask()
{
    //Fill the list with data as above, code deleted here for brevity
    ...
    //Raise the event to signal that the work is done
    if (OnWorkDone != null)
    {
        OnWorkDone(new EventArgs());
    }
}

Now, all that remains is for the form to register the event and respond to it. Back in the button handler where we run the background Thread, insert a new line between instantiating the background class and instantiating the Thread (I'll repeat those lines here). On the new line, type the start of the line shown here...

C#
BackgroundWorkThreadClass bwt = new BackgroundWorkThreadClass(ResultList);
bwt.OnWorkDone += bgWork = new Thread(new ThreadStart(bwt.DoLongTask));

After you type the "=" sign, Visual Studio will smartly show something like this:

Sample Image - maximum width is 600 pixels

Hit the Tab key, and it will switch to something like this:

Sample Image - maximum width is 600 pixels

Hit the Tab once more, and you get all of this (not guaranteed to be in this order, but it will be there):

C#
private void btnStart_Click(object sender, EventArgs e)
{
    BackgroundWorkThreadClass bwt = new BackgroundWorkThreadClass(ResultList);
    bwt.OnWorkDone += new BgWorkDoneHandler(bwt_OnWorkDone);
    bgWork = new Thread(new ThreadStart(bwt.DoLongTask));
    bgWork.Start();
    lblStstus.Text = "Thread started";
}

void bwt_OnWorkDone(EventArgs e)
{
    throw new Exception("The method is not implemented.");
}

Notice again that we (or VS, in this case) omitted the () when passing the bwt_OnWorkDone() method to the handler constructor. Now, obviously, we need to do something in the event handler besides throwing an exception. If we want to do something internal, we can do it right here in the event handler. Say, we have made a boolean field at the top of the class and called it ListIsReady. We can set ListIsReady = true right here, and it is considered thread safe. But, we want to update the GUI, so we need to have a separate method and use BeginInvoke() on it, and to do that, we have to declare a delegate. Back at the top of the form class where we declared our List<> and our Thread, we need to add this:

C#
private delegate void UpdFromEventDelegate();

Then, we can add our GUI update in a new method, and invoke that method through the delegate, once again skipping the ().

C#
void bwt_OnWorkDone(EventArgs e)
{
    //Non-GUI code is safe to run right here
    BeginInvoke(new UpdFromEventDelegate(UpdFromEventMethod));
}

private void UpdFromEventMethod()
{
    lblStstus.Text = "Thread done";
}

And, there you have it. The form creates a background worker, launches it, gets notified when the work is complete, and updates its own GUI when it gets that notification.

Points of interest

I wrote this article in self defense. I don't need to do this very often, and always forget just how to do it. I never like the way anyone explains it, so I end up reading several articles to remember how. Hopefully, you will find this step by step presentation to be helpful. I know I will, next time I need to do it and forgot how again.

I learned that when you write an article for CP, you may just have a better way suggested to you...see Update.

Update

Thanks to a good comment from PIEBALDconsult that made me think, here is how it works all in one class. I actually like this better now that I know I can check InvokeRequired and then Invoke a method from within itself. Note that as above, we omit the () from the method name we reference in new ThreadStart(bgTask) and new MethodInvoker(setLabel).

C#
//Create and launch the thread
private void Form2_Load(object sender, EventArgs e)
{
    Thread t = new Thread(new ThreadStart(bgTask));
    t.Start();
}
//This is the method that runs in the background
//It calls a method at the end which updates the GUI
private void bgTask()
{
    Thread.Sleep(5000);
    setLabel();
}
//The GUI update method Invokes itself if needed
private void setLabel()
{
    if (this.InvokeRequired)
    {
        this.Invoke(new MethodInvoker(setLabel));
    }
    else
    {
        label2.Text = "done";
    }
}

License

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


Written By
Software Developer SIRVA
United States United States
SIRVACodeGuy (formerly AWSCodeGuy) has been an IT pro for over ten years.
Starting with an A+ certification, it took him only a few months to get kicked out of hardware and into EDI data analysis, where he learned VBA in MS Excel 97 as a matter of self-preservation ("There's GOTTA be an easier way to do this!").
After a year of that role growing into support of an Access 97 MRP/EDI package, a project came up that required Java, so he set out to learn that, since there was nobody in the company who understood Java. The course at IVY Tech was fun, so he went back for VB.Net 1.0 and SQL Server.
That led to a new job writing Windows applications, where he learned about users who have no clue what they want and the concept of ever-changing specs and features.
A couple years of that led to a position writing web applications with ASP.Net and VB.Net 2.0, and some AJAX, Windows forms in C# 2.0, SQL Server Report Services.
Now working for SIRVA using all sorts of obscure, arcane technology like writing a .Net 3.5 dll which consumes one VB6 COM+ module and is consumed by a different VB6 COM+ module.

Comments and Discussions

 
QuestionBackGroundWorker Pin
marco_br3-Jun-08 6:52
marco_br3-Jun-08 6:52 
AnswerRe: BackGroundWorker Pin
SIRVACodeGuy3-Jun-08 7:33
SIRVACodeGuy3-Jun-08 7:33 
GeneralRe: BackGroundWorker Pin
PIEBALDconsult3-Jun-08 18:15
mvePIEBALDconsult3-Jun-08 18:15 
GeneralRe: BackGroundWorker Pin
marco_br4-Jun-08 2:28
marco_br4-Jun-08 2:28 
PIEBALDconsult wrote:
Have you tried Command.Cancel() ? (I haven't.)


I haven't too, but, to be honest, up to now in my application I don't have a query that takes much long. Anyway, thanks for the heads up. It might be useful in the future... Cool | :cool:


PIEBALDconsult wrote:
That's what I do. The query itself may still take a while to complete, but I can read through the DataReader logging every so many records and/or so many seconds, so I can see that it's doing something.


Would you mind to post an example (code snippet) of how do you do that?


Regards
GeneralRe: BackGroundWorker Pin
PIEBALDconsult4-Jun-08 5:50
mvePIEBALDconsult4-Jun-08 5:50 
GeneralRe: BackGroundWorker Pin
marco_br4-Jun-08 7:05
marco_br4-Jun-08 7:05 
GeneralRe: BackGroundWorker Pin
PIEBALDconsult4-Jun-08 7:21
mvePIEBALDconsult4-Jun-08 7:21 
AnswerRe: BackGroundWorker Pin
SIRVACodeGuy4-Jun-08 7:22
SIRVACodeGuy4-Jun-08 7:22 
AnswerRe: BackGroundWorker Pin
marco_br5-Jun-08 7:48
marco_br5-Jun-08 7:48 
GeneralRe: BackGroundWorker Pin
SIRVACodeGuy4-Jun-08 2:31
SIRVACodeGuy4-Jun-08 2:31 
GeneralRe: BackGroundWorker Pin
marco_br4-Jun-08 2:19
marco_br4-Jun-08 2:19 
GeneralRe: BackGroundWorker Pin
SIRVACodeGuy4-Jun-08 2:40
SIRVACodeGuy4-Jun-08 2:40 
AnswerRe: BackGroundWorker Pin
marco_br4-Jun-08 4:06
marco_br4-Jun-08 4:06 
GeneralGood Tutorial Pin
merlin9812-Jun-08 4:39
professionalmerlin9812-Jun-08 4:39 
GeneralWell, that's one way... Pin
PIEBALDconsult31-May-08 17:27
mvePIEBALDconsult31-May-08 17:27 
GeneralRe: Well, that's one way... Pin
supercat91-Jun-08 9:31
supercat91-Jun-08 9:31 
GeneralRe: Well, that's one way... Pin
PIEBALDconsult1-Jun-08 17:37
mvePIEBALDconsult1-Jun-08 17:37 
GeneralRe: Well, that's one way... Pin
supercat91-Jun-08 18:03
supercat91-Jun-08 18:03 
GeneralRe: Well, that's one way... Pin
PIEBALDconsult1-Jun-08 19:08
mvePIEBALDconsult1-Jun-08 19:08 
GeneralRe: Well, that's one way... Pin
supercat92-Jun-08 16:44
supercat92-Jun-08 16:44 
GeneralRe: Well, that's one way... Pin
PIEBALDconsult2-Jun-08 17:31
mvePIEBALDconsult2-Jun-08 17:31 
GeneralRe: Well, that's one way... Pin
supercat93-Jun-08 5:07
supercat93-Jun-08 5:07 
AnswerRe: Well, that's one way... Pin
SIRVACodeGuy2-Jun-08 2:33
SIRVACodeGuy2-Jun-08 2:33 
GeneralRe: Well, that's one way... Pin
PIEBALDconsult2-Jun-08 5:39
mvePIEBALDconsult2-Jun-08 5:39 
GeneralRe: Well, that's one way... Pin
SIRVACodeGuy2-Jun-08 2:20
SIRVACodeGuy2-Jun-08 2:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.