Bagikan melalui


Walkthrough: Authoring a Simple Multithreaded Component with Visual C#

The BackgroundWorker component replaces and adds functionality to the System.Threading namespace; however, the System.Threading namespace is retained for both backward compatibility and future use, if you choose. For more information, see BackgroundWorker Component Overview.

You can write applications that are able to perform multiple tasks simultaneously. This ability, called multithreading, or free threading, is a powerful way to design components that are processor-intensive and require user input. An example of a component that might make use of multithreading would be a component that calculates payroll information. The component could process data entered into a database by a user on one thread while processor-intensive payroll calculations were performed on another. By running these processes on separate threads, users do not need to wait for the computer to complete calculations before entering additional data. In this walkthrough, you will create a simple multithreaded component that performs several complex calculations simultaneously.

Creating the Project

Your application will consist of a single form and a component. The user will input values and signal to the component to begin calculations. The form will then receive values from your component and display them in label controls. The component will perform the processor-intensive calculations and signal the form when complete. You will create public variables in your component to hold values received from your user interface. You will also implement methods in your component to perform the calculations based on the values of these variables.

Note

Although a function is usually preferable for a method that calculates a value, arguments cannot be passed between threads, nor can values be returned. There are many simple ways to supply values to threads and to receive values from them. In this demonstration, you will return values to your user interface by updating public variables, and events will be used to notify the main program when a thread has completed execution.

The dialog boxes and menu commands you see might differ from those described in Help depending on your active settings or edition. To change your settings, choose Import and Export Settings on the Tools menu. For more information, see Working with Settings.

To create the form

  1. Create a new Windows Application project.

  2. Name the application Calculations and rename Form1.cs as frmCalculations.cs. When Visual Studio prompts you to rename the Form1 code element, click Yes.

    This form will serve as the primary user interface for your application.

  3. Add five Label controls, four Button controls, and one TextBox control to your form.

  4. Set properties for these controls as follows:

    Control

    Name

    Text

    label1

    lblFactorial1

    (blank)

    label2

    lblFactorial2

    (blank)

    label3

    lblAddTwo

    (blank)

    label4

    lblRunLoops

    (blank)

    label5

    lblTotalCalculations

    (blank)

    button1

    btnFactorial1

    Factorial

    button2

    btnFactorial2

    Factorial - 1

    button3

    btnAddTwo

    Add Two

    button4

    btnRunLoops

    Run a Loop

    textBox1

    txtValue

    (blank)

To create the Calculator component

  1. From the Project menu, select Add Component.

  2. Name the component Calculator.

To add public variables to the Calculator component

  1. Open the Code Editor for Calculator.

  2. Add statements to create public variables that you will use to pass values from frmCalculations to each thread.

    The variable varTotalCalculations will keep a running total of the total number of calculations performed by the component, and the other variables will receive values from the form.

    public int varAddTwo; 
    public int varFact1;
    public int varFact2;
    public int varLoopValue;
    public double varTotalCalculations = 0;
    

To add methods and events to the Calculator component

  1. Declare the delegates for the events that your component will use to communicate values to your form.

    Note

    Although you will be declaring four events, you only need to create three delegates, because two events will have the same signature.

    Immediately beneath the variable declarations entered in the previous step, type the following code:

    // This delegate will be invoked with two of your events.
    public delegate void FactorialCompleteHandler(double Factorial, double TotalCalculations);
    public delegate void AddTwoCompleteHandler(int Result, double TotalCalculations);
    public delegate void LoopCompleteHandler(double TotalCalculations, int Counter);
    
  2. Declare the events that your component will use to communicate with your application. Do this by adding the following code immediately beneath the code entered in the previous step.

    public event FactorialCompleteHandler FactorialComplete;
    public event FactorialCompleteHandler FactorialMinusOneComplete;
    public event AddTwoCompleteHandler AddTwoComplete;
    public event LoopCompleteHandler LoopComplete;
    
  3. Immediately beneath the code you typed in the previous step, type the following code:

    // This method will calculate the value of a number minus 1 factorial
    // (varFact2-1!).
    public void FactorialMinusOne()
    {
       double varTotalAsOfNow = 0;
       double varResult = 1;
       // Performs a factorial calculation on varFact2 - 1.
       for (int varX = 1; varX <= varFact2 - 1; varX++)
       {
          varResult *= varX;
          // Increments varTotalCalculations and keeps track of the current 
          // total as of this instant.
          varTotalCalculations += 1;
          varTotalAsOfNow = varTotalCalculations;
       }
       // Signals that the method has completed, and communicates the 
       // result and a value of total calculations performed up to this 
       // point.
       FactorialMinusOneComplete(varResult, varTotalAsOfNow);
    }
    
    // This method will calculate the value of a number factorial.
    // (varFact1!)
    public void Factorial()
    {
       double varResult = 1;
       double varTotalAsOfNow = 0;
       for (int varX = 1; varX <= varFact1; varX++)
       {
          varResult *= varX;
          varTotalCalculations += 1;
          varTotalAsOfNow = varTotalCalculations;
       }
       FactorialComplete(varResult, varTotalAsOfNow);
    }
    
    // This method will add two to a number (varAddTwo+2).
    public void AddTwo()
    {
       double varTotalAsOfNow = 0;  
       int varResult = varAddTwo + 2;
       varTotalCalculations += 1;
       varTotalAsOfNow = varTotalCalculations;
       AddTwoComplete(varResult, varTotalAsOfNow);
    }
    
    // This method will run a loop with a nested loop varLoopValue times.
    public void RunALoop()
    {
       int varX;
       double varTotalAsOfNow = 0;
       for (varX = 1; varX <= varLoopValue; varX++)
       {
        // This nested loop is added solely for the purpose of slowing down
        // the program and creating a processor-intensive application.
          for (int varY = 1; varY <= 500; varY++)
          {
             varTotalCalculations += 1;
             varTotalAsOfNow = varTotalCalculations;
          }
       }
       LoopComplete(varTotalAsOfNow, varLoopValue);
    }
    

Transferring User Input to the Component

The next step is to add code to frmCalculations to receive input from the user and to transfer and receive values to and from the Calculator component.

To implement front-end functionality to frmCalculations

  1. Open frmCalculations in the Code Editor.

  2. Locate the public partial class frmCalculations statement. Immediately beneath the { type:

    Calculator Calculator1;
    
  3. Locate the constructor. Immediately before the }, add the following line:

    // Creates a new instance of Calculator.
    Calculator1 = new Calculator();
    
  4. In the designer, click each button to generate the code outline for each control's Click event handlers and add code to create the handlers.

    When complete, your Click event handlers should resemble the following:

    // Passes the value typed in the txtValue to Calculator.varFact1.
    private void btnFactorial1_Click(object sender, System.EventArgs e)
    {
       Calculator1.varFact1 = int.Parse(txtValue.Text);
       // Disables the btnFactorial1 until this calculation is complete.
       btnFactorial1.Enabled = false;
       Calculator1.Factorial();
    }
    
    private void btnFactorial2_Click(object sender, System.EventArgs e)
    {
       Calculator1.varFact2 = int.Parse(txtValue.Text);
       btnFactorial2.Enabled = false;
       Calculator1.FactorialMinusOne();
    }
    private void btnAddTwo_Click(object sender, System.EventArgs e)
    {
       Calculator1.varAddTwo = int.Parse(txtValue.Text);
       btnAddTwo.Enabled = false;
       Calculator1.AddTwo();
    }
    private void btnRunLoops_Click(object sender, System.EventArgs e)
    {
       Calculator1.varLoopValue = int.Parse(txtValue.Text);
       btnRunLoops.Enabled = false;
       // Lets the user know that a loop is running
       lblRunLoops.Text = "Looping";
       Calculator1.RunALoop();
    }
    
  5. After the code you added in the previous step, type the following to handle the events your form will receive from Calculator1:

    private void FactorialHandler(double Value, double Calculations)
    // Displays the returned value in the appropriate label.
    {
       lblFactorial1.Text = Value.ToString();
       // Re-enables the button so it can be used again.
       btnFactorial1.Enabled = true;
       // Updates the label that displays the total calculations performed
       lblTotalCalculations.Text = "TotalCalculations are " + 
          Calculations.ToString();
    }
    
    private void FactorialMinusHandler(double Value, double Calculations)
    {
       lblFactorial2.Text = Value.ToString();
       btnFactorial2.Enabled = true;
       lblTotalCalculations.Text = "TotalCalculations are " + 
          Calculations.ToString();
    }
    
    private void AddTwoHandler(int Value, double Calculations)
    {
       lblAddTwo.Text = Value.ToString();
       btnAddTwo.Enabled = true;
       lblTotalCalculations.Text = "TotalCalculations are " +
          Calculations.ToString();
    }
    
    private void LoopDoneHandler(double Calculations, int Count)
    {
       btnRunLoops.Enabled = true;
       lblRunLoops.Text = Count.ToString();
       lblTotalCalculations.Text = "TotalCalculations are " +
          Calculations.ToString();
    }
    
  6. In the constructor of frmCalculations, add the following code immediately before the } to handle the custom events your form will receive from Calculator1.

    Calculator1.FactorialComplete += new
       Calculator.FactorialCompleteHandler(this.FactorialHandler);
    Calculator1.FactorialMinusOneComplete += new
       Calculator.FactorialCompleteHandler(this.FactorialMinusHandler);
    Calculator1.AddTwoComplete += new
       Calculator.AddTwoCompleteHandler(this.AddTwoHandler);
    Calculator1.LoopComplete += new
       Calculator.LoopCompleteHandler(this.LoopDoneHandler);
    

Testing Your Application

You have now created a project that incorporates a form and a component capable of performing several complex calculations. Although you have not implemented multithreading capability yet, you will test your project to verify its functionality before proceeding.

To test your project

  1. From the Debug menu, choose Start Debugging.

    The application begins and frmCalculations appears.

  2. In the text box, type 4, then click the button labeled Add Two.

    The numeral "6" should be displayed in the label beneath it, and "Total Calculations are 1" should be displayed in lblTotalCalculations.

  3. Now click the button labeled Factorial - 1.

    The numeral "6" should be displayed beneath the button, and lblTotalCalculations now reads "Total Calculations are 4."

  4. Change the value in the text box to 20, then click the button labeled Factorial.

    The number "2.43290200817664E+18" is displayed beneath it, and lblTotalCalculations now reads "Total Calculations are 24."

  5. Change the value in the text box to 50000, and then click the button labeled Run A Loop.

    Note that there is a small but noticeable interval before this button is re-enabled. The label under this button should read "50000" and the total calculations displayed are "25000024."

  6. Change the value in the text box to 5000000 and click the button labeled Run A Loop, then immediately click the button labeled Add Two. Click it again.

    The button does not respond, nor will any control on the form respond until the loops have been completed.

    If your program runs only a single thread of execution, processor-intensive calculations such as the above example have the tendency to tie up the program until the calculations have been completed. In the next section, you will add multithreading capability to your application so that multiple threads may be run at once.

Adding Multithreading Capability

The previous example demonstrated the limitations of applications that run only a single thread of execution. In the next section you will use the Thread class to add multiple threads of execution to your component.

To add the Threads subroutine

  1. Open Calculator.cs in the Code Editor.

  2. Near the top of the code, locate the class declaration, and immediately beneath the {, type the following:

    // Declares the variables you will use to hold your thread objects.
    public System.Threading.Thread FactorialThread; 
    public System.Threading.Thread FactorialMinusOneThread;  
    public System.Threading.Thread AddTwoThread; 
    public System.Threading.Thread LoopThread;
    
  3. Immediately before the end of the class declaration at the bottom of the code, add the following method:

    public void ChooseThreads(int threadNumber)
    {
    // Determines which thread to start based on the value it receives.
    switch(threadNumber)
       {
          case 1:
             // Sets the thread using the AddressOf the subroutine where
             // the thread will start.
             FactorialThread = new System.Threading.Thread(new
                System.Threading.ThreadStart(this.Factorial));
             // Starts the thread.
             FactorialThread.Start();
             break;
          case 2:
             FactorialMinusOneThread = new
                System.Threading.Thread(new
                   System.Threading.ThreadStart(this.FactorialMinusOne));
             FactorialMinusOneThread.Start();
             break;
          case 3:
             AddTwoThread = new System.Threading.Thread(new
                 System.Threading.ThreadStart(this.AddTwo));
             AddTwoThread.Start();
             break;
          case 4:
             LoopThread = new System.Threading.Thread(new
                System.Threading.ThreadStart(this.RunALoop));
             LoopThread.Start();
             break;
       }
    }
    

    When a Thread is instantiated, it requires an argument in the form of a ThreadStart. The ThreadStart is a delegate that points to the address of the method where the thread is to begin. A ThreadStart cannot take parameters or pass values, and therefore can only indicate a void method. The ChooseThreads method you just implemented will receive a value from the program calling it and use that value to determine the appropriate thread to start.

To add the appropriate code to frmCalculations

  1. Open the frmCalculations.cs file in the Code Editor, then locate private void btnFactorial1_Click.

    1. Comment out the line that calls the Calculator1.Factorial1 method directly as shown:

      // Calculator1.Factorial()
      
    2. Add the following line to call the Calculator1.ChooseThreads method:

      // Passes the value 1 to Calculator1, thus directing it to start the 
      // correct thread.
      Calculator1.ChooseThreads(1);
      
  2. Make similar modifications to the other button_click methods.

    Note

    Be certain to include the appropriate value for the Threads argument.

    When you have finished, your code should look similar to the following:

    private void btnFactorial1_Click(object sender, System.EventArgs e)
    // Passes the value typed in the txtValue to Calculator.varFact1
    {
       Calculator1.varFact1 = int.Parse(txtValue.Text);
       // Disables the btnFactorial1 until this calculation is complete
       btnFactorial1.Enabled = false;
       // Calculator1.Factorial();
       Calculator1.ChooseThreads(1);
    }
    
    private void btnFactorial2_Click(object sender, System.EventArgs e)
    {
       Calculator1.varFact2 = int.Parse(txtValue.Text); 
       btnFactorial2.Enabled = false;         
       // Calculator1.FactorialMinusOne();
       Calculator1.ChooseThreads(2);
    }
    private void btnAddTwo_Click(object sender, System.EventArgs e)
    {
       Calculator1.varAddTwo = int.Parse(txtValue.Text);
       btnAddTwo.Enabled = false;
       // Calculator1.AddTwo();
       Calculator1.ChooseThreads(3);
    }
    
    private void btnRunLoops_Click(object sender, System.EventArgs e)
    {
       Calculator1.varLoopValue = int.Parse(txtValue.Text);
       btnRunLoops.Enabled = false;
       // Lets the user know that a loop is running
       lblRunLoops.Text = "Looping";
       // Calculator1.RunALoop();
       Calculator1.ChooseThreads(4);
    }
    

Marshaling Calls to Controls

You will now facilitate updating the display on the form. Because controls are always owned by the main thread of execution, any call to a control from a subordinate thread requires a marshaling call. Marshaling is the act of moving a call across thread boundaries, and is very expensive in terms of resources. To minimize the amount of marshaling that needs to occur, and to be sure that your calls are handled in a thread-safe manner, you will use the Control.BeginInvoke method to invoke methods on the main thread of execution, thereby minimizing the amount of cross-thread-boundary marshaling that must occur. This kind of call is necessary when calling methods that manipulate controls. For details, see How to: Manipulate Controls from Threads.

To create the control-invoking procedures

  1. Open the code editor for frmCalculations. In the declarations section, add the following code:

    public delegate void FHandler(double Value, double Calculations);
    public delegate void A2Handler(int Value, double Calculations);
    public delegate void LDHandler(double Calculations, int Count);
    

    Invoke and BeginInvoke require a delegate to the appropriate method as an argument. These lines declare the delegate signatures that will be used by BeginInvoke to invoke the appropriate methods.

  2. Add the following empty methods to your code.

    public void FactHandler(double Value, double Calculations)
    {
    }
    public void Fact1Handler(double Value, double Calculations)
    {
    }
    public void Add2Handler(int Value, double Calculations)
    {
    }
    public void LDoneHandler(double Calculations, int Count)
    {
    }
    
  3. From the Edit menu, use Cut and Paste to cut all the code from the method FactorialHandler and paste it into FactHandler.

  4. Repeat the previous step for FactorialMinusHandler and Fact1Handler, AddTwoHandler and Add2Handler, and LoopDoneHandler and LDoneHandler.

    When finished, there should be no code remaining in FactorialHandler, Factorial1Handler, AddTwoHandler, and LoopDoneHandler, and all the code these used to contain should have been moved to the appropriate new methods.

  5. Call the BeginInvoke method to invoke the methods asynchronously. You can call BeginInvoke from either your form (this) or any of the controls on the form.

    When complete, your code should look similar to the following:

    protected void FactorialHandler(double Value, double Calculations)
    {
       // BeginInvoke causes asynchronous execution to begin at the address
       // specified by the delegate. Simply put, it transfers execution of 
       // this method back to the main thread. Any parameters required by 
       // the method contained at the delegate are wrapped in an object and 
       // passed. 
       this.BeginInvoke(new FHandler(FactHandler), new Object[]
          {Value, Calculations});
    }
    protected void FactorialMinusHandler(double Value, double Calculations)
    {
       this.BeginInvoke(new FHandler(Fact1Handler), new Object []
          {Value, Calculations});
    }
    
    protected void AddTwoHandler(int Value, double Calculations)
    {
       this.BeginInvoke(new A2Handler(Add2Handler), new Object[]
          {Value, Calculations});
    }
    
    protected void LoopDoneHandler(double Calculations, int Count)
    {
       this.BeginInvoke(new LDHandler(LDoneHandler), new Object[]
          {Calculations, Count});
    }
    

    It may seem as though the event handler is simply making a call to the next method. The event handler is actually causing a method to be invoked on the main thread of operation. This approach saves on calls across thread boundaries and allows your multithreaded applications to run efficiently and without fear of causing lockup. For details on working with controls in a multithreaded environment, see How to: Manipulate Controls from Threads.

  6. Save your work.

  7. Test your solution by choosing Start Debugging from the Debug menu.

    1. Type 10000000 in the text box and click Run A Loop.

      "Looping" is displayed in the label beneath this button. This loop should take a significant amount of time to run. If it completes too early, adjust the size of the number accordingly.

    2. In rapid succession, click all three buttons that are still enabled. You will find that all buttons respond to your input. The label beneath Add Two should be the first to display a result. Results will later be displayed in the labels beneath the factorial buttons. These results evaluate to infinity, as the number returned by a 10,000,000 factorial is too large for a double-precision variable to contain. Finally, after an additional delay, results are returned beneath the Run A Loop button.

      As you just observed, four separate sets of calculations were performed simultaneously upon four separate threads. The user interface remained responsive to input, and results were returned after each thread completed.

Coordinating Your Threads

An experienced user of multithreaded applications may perceive a subtle flaw with the code as typed. Recall the lines of code from each calculation-performing method in Calculator:

varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;

These two lines of code increment the public variable varTotalCalculations and set the local variable varTotalAsOfNow to this value. This value is then returned to frmCalculations and displayed in a label control. But is the correct value being returned? If only a single thread of execution is running, the answer is clearly yes. But if multiple threads are running, the answer becomes more uncertain. Each thread has the ability to increment the variable varTotalCalculations. It is possible that after one thread increments this variable, but before it copies the value to varTotalAsOfNow, another thread could alter the value of this variable by incrementing it. This leads to the possibility that each thread is, in fact, reporting inaccurate results. Visual C# provides the lock Statement (C# Reference) to allow synchronization of threads to ensure that each thread always returns an accurate result. The syntax for lock is as follows:

lock(AnObject)
{
   // Insert code that affects the object.
   // Insert more code that affects the object.
   // Insert more code that affects the object.
// Release the lock.
}

When the lock block is entered, execution on the specified expression is blocked until the specified thread has an exclusive lock on the object in question. In the example shown above, execution is blocked on AnObject. lock must be used with an object that returns a reference rather than a value. The execution may then proceed as a block without interference from other threads. A set of statements that execute as a unit are said to be atomic. When the } is encountered, the expression is freed and the threads are allowed to proceed normally.

To add the lock statement to your application

  1. Open Calculator.cs in the Code Editor.

  2. Locate each instance of the following code:

    varTotalCalculations += 1;
    varTotalAsOfNow = varTotalCalculations;
    

    There should be four instances of this code, one in each calculation method.

  3. Modify this code so that it reads as follows:

    lock(this)
    {
       varTotalCalculations += 1;
       varTotalAsOfNow = varTotalCalculations;
    }
    
  4. Save your work and test it as in the previous example.

    You may notice a slight impact on the performance of your program. This is because execution of threads stops when an exclusive lock is obtained on your component. Although it ensures accuracy, this approach impedes some of the performance benefits of multiple threads. You should carefully consider the need for locking threads, and implement them only when absolutely necessary.

See Also

Tasks

How to: Coordinate Multiple Threads of Execution

Walkthrough: Authoring a Simple Multithreaded Component with Visual Basic

Reference

BackgroundWorker

Concepts

Event-based Asynchronous Pattern Overview

Other Resources

Programming with Components

Component Programming Walkthroughs

Multithreading in Components