You can write applications that can execute multiple tasks at the same time. This capability (called "multi-threaded processing" or "free-threaded processing") is a powerful method for designing processor-intensive components that require user input. The component used to calculate the payroll information is an example of a component that may be processed using multiple threads. This component can process user input to the database data in one thread, and execute payroll calculations that frequently use processors on the other thread. By running these processes on different threads, you can enter other data without waiting for the computer to complete the computation. In this walkthrough, a simple multi-threaded component is created, which can execute several complex computations at the same time.
Create a project
The application will include a single form and a component. The user enters a value and instructs the component to start calculation. The form then receives the value from this component and displays it in the tag control. This component will execute frequent processor computing and notify the form after completion. You will create a public variable in the component to save the value received from the user interface. At the same time, you will also implement some methods in the component to perform calculations based on the values of these variables.
Note:Although a function is generally more desirable for calculating values, it cannot pass parameters between threads or return values. There are many simple methods to provide the value to the thread and receive the value from the thread. In this demonstration, the value is returned to the user interface by updating the public variables. After the thread is executed, the main program is notified by events.
Create a form
- Create a new "Windows application" project.
- Name the applicationCalculations, AndForm1.csRenameFrmcalculations. CS.
This form is used as the main user interface of the application.
- Double-click the form on the designer to open the code editor. In the "edit" menu, select "search and replace", and then select "replace ". Replace allForm1ReplaceFrmcalculations.
- In Solution Explorer, right-click "frmcalculations. cs" and select "view designer ". Open the designer.
- Add 5 to the formLabelControls, 4ButtonControls and 1TextboxControl.
- Set Properties for these controls as follows:
Widget |
Name |
Text |
Label1 |
Lblfactorial1 |
(Blank) |
Label2 |
Lblfactorial |
(Blank) |
Label3 |
Lbladdtwo |
(Blank) |
Label4 |
Lblrunloops |
(Blank) |
Label5 |
Lbltotalcalculations |
(Blank) |
Button1 |
Btnfactorial1 |
Factorial |
Button2 |
Btnfactorial |
Factorial-1 |
Button3 |
Btnaddtwo |
Add two |
Button4 |
Btnrunloops |
Run a loop |
Textbox1 |
Txtvalue |
(Blank) |
Create a calculator component
- Select "add component" from the "project" menu ".
- Name the componentCalculator.
Add public variables to the calculator component
- IsCalculatorOpen the code editor.
- Add the statement for creating public variables. These variables are used to extract values fromFrmcalculationsPassed to each thread.
The variable vartotalcalculations retains the cumulative value of the total number of computations executed by this component, while other variables receive values from the form.
public int varAddTwo; public int varFact1;public int varFact2;public int varLoopValue;public double varTotalCalculations = 0;
Add methods and events to the calculator component
- Delegates an event declaration. The component uses these events to pass values to the form.
Note:Although you will declare four events, you only need to create three delegates because the two events have the same signature.
Next, enter the following code at the bottom of the variable declaration entered in the previous step:
// 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);
- Declare events that the component will use to communicate with the application. To achieve this, add the following code below the code entered in the previous step.
public event FactorialCompleteHandler FactorialComplete;public event FactorialCompleteHandler FactorialMinusOneComplete;public event AddTwoCompleteHandler AddTwoComplete;public event LoopCompleteHandler LoopComplete;
- Next, enter the following code at the bottom of the code you typed in the previous step:
// 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);}
Transmit user input to components
The next step isFrmcalculationsAdd code to receive user input, receive and transmit values from and to the calculator component.
Implement the front-end functions of frmcalculations
- OpenFrmcalculations.
- Find
public class frmCalculations
Statement. Enter the following:Calculator Calculator1;
- Find the constructor. Then add the following lines before:
// Creates a new instance of Calculator.Calculator1 = new Calculator();
- Click each button in the designer to generate an outline of the code for each control's click event handler, and add code to create these handlers.
After that, click the event handler in the following format:
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();}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();}
- At the bottom of the code added in the previous step, type the following code to processCalculator1Events received:
protected 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();}protected void FactorialMinusHandler(double Value, double Calculations){ lblFactorial2.Text = Value.ToString(); btnFactorial2.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString();}protected void AddTwoHandler(int Value, double Calculations){ lblAddTwo.Text = Value.ToString(); btnAddTwo.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString();}protected void LoopDoneHandler(double Calculations, int Count){ btnRunLoops.Enabled = true; lblRunLoops.Text = Count.ToString(); lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString();}
- InFrmcalculationsIn the constructor}Add the following code to process the form fromCalculator1Custom events received:
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);
Test the application
Now that the project has been created, the project will be able to execute several complex computing components and forms together. Although multi-threaded processing has not yet been implemented, the project should be tested to verify its functionality before continuing.
Test Item
- Select Start from the Debug menu ".
Application startup and displayFrmcalculations.
- In the text box, type4And then click the button marked as "add two.
The number "6" should be displayed in the label at the bottom of the button.Lbltotalcalculations"Total calculations are 1" should be displayed ".
- Click the button marked as "factorial-1.
The number "6" should be displayed at the bottom of the button, whileLbltotalcalculations"Total calculations are 4" should be displayed ".
- Change the value in the text box20And then click the button marked as "factorial.
"2.43290200817664e + 18" is displayed at the bottom of the button.LbltotalcalculationsTotal calculations are 24 ".
- Change the value in the text box50000And then click the button marked as "Running loop.
Note that there is a short but obvious interval before this button is re-enabled. "50000" should be displayed for the tag under this button, and "25000024" is displayed for the total number of computations ".
- Change the value in the text box5000000Click the button marked as "running cycle", and then click the button marked as "add two. Click it again.
The button and any controls on the form will respond until the loop is completed.
If the program runs only one execution thread, computation similar to the preceding example that frequently uses the processor tends to occupy the program until the computation is completed. In the next section, you will add a multi-threaded processing function to the application so that multiple threads can run at a time.
Add multi-thread processing
The preceding example demonstrates the restrictions on applications that only run a single execution thread. In the next section, you will use Object To add multiple execution threads to the component.
Add threads subroutine
- OpenCalculator. CS.
- Near the top of the Code, locate the class declaration, and enter the following code at the bottom:
// 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;
- Add the following method before the end of the class declaration at the bottom of the Code:
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 instantiatingThreadObject, it requiresThreadstartParameters in the format of objects.ThreadstartAn object is a delegate to the address of the method that points to the start thread.ThreadstartThe object cannot accept parameters or pass values, so it can only representVoidMethod. TheChoosethreadsThe method will receive a value from the program that calls it, and use this value to determine the appropriate thread to start.
Add appropriate code to frmcalculations
- OpenFrmcalculations. CSFile, and then find protected void btnfactorial?click.
- Comment out direct callCalculator1.factorial1The method line is as follows:
// Calculator1.Factorial()
- Add the following lines to callCalculator1.choosethreadsMethod:
// Passes the value 1 to Calculator1, thus directing it to start the // correct thread.Calculator1.ChooseThreads(1);
- Make similar changes to other button_click child routines.
Note:Must be
ThreadsThe parameter contains the appropriate value.
The code looks like the following:
protected 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);}protected void btnFactorial2_Click(object sender, System.EventArgs e){ Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; // Calculator1.FactorialMinusOne(); Calculator1.ChooseThreads(2);}protected void btnAddTwo_Click(object sender, System.EventArgs e){ Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; // Calculator1.AddTwo(); Calculator1.ChooseThreads(3);}protected 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);}
Sending and receiving controls
Now the display on the acceleration form is updated. Since the control is always owned by the main execution thread, any call to the control in the slave thread must be called by "sending and processing. Sending and receiving is a behavior that moves a call across thread boundaries and consumes a lot of resources. Use To call the methods on the main execution thread, so that the amount of messages sent across the thread boundary must be minimized. This call is necessary when you call methods for operation controls. For more information, see operate controls from a thread.
Create a control call Process
- IsFrmcalculationsOpen the code editor. In the declaration 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);
InvokeAndBegininvokeThe delegate of the appropriate method must be used as a parameter. These code lines declare some delegate signatures, and these signatures will beBegininvokeUsed to call an appropriate method.
- Add the following empty methods to the 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){}
- In the edit menu, use cut and paste to cut all the code from the factorialhandler method and paste it into facthandler.
- Repeat the preceding steps for factorialminushandler, fact1handler, addtwohandler, add2handler, loopdonehandler, and ldonehandler.
After completion, there should be no remaining code in factorialhandler, factorial1handler, addtwohandler, and loopdonehandler, and all the code they contained should have been moved to the appropriate new method.
- CallBegininvokeMethods call these methods asynchronously. From the form (This) Or call any controls on the form.Begininvoke.
The code looks like 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 seems that the event handler only calls the next method. In fact, the event handler calls the method on the main operation thread. This method saves calls across thread boundaries and enables multi-threaded applications to run effectively without worrying about deadlock. For more information about using controls in a multi-threaded environment, see operate controls from a thread.
- Save your work.
- Select Start from the Debug menu to test the solution.
- Type10000000Click "running cycle ".
"Looping" is displayed in the label below this button ". It takes a long time to run this loop. If it completes too quickly, adjust the size of the number accordingly.
- Click the three buttons that are enabled continuously. You will find that all buttons respond to your input. The label under "add two" should be the first display result. The result will be displayed in the label below the factorial button later. It is estimated that these results will be infinitely large, because the number returned by a 10,000,000 factorial is too large for a double-precision variable to exceed its range. Finally, the result will be returned to the lower part of the "Running loop" button.
As we have just observed, four Independent Computing groups are executed simultaneously on four separate threads. The User Interface maintains a response to the input and returns the result after each thread completes.
Coordination thread
Experienced multi-threaded applications may find subtle defects in the entered code. SlaveCalculator. CSIn each of the Child routines that execute the calculation, recall the following code lines:
varTotalCalculations += 1;varTotalAsOfNow = varTotalCalculations;
The two lines of code increase by public variablesVartotalcalculationsAnd set the local variableVartotalasofnowSet this value. Then, the value is returnedFrmcalculationsAnd displayed in the label control. But is the returned value correct? If only one execution thread is running, the answer is obviously correct. However, if multiple threads are running, the answer becomes uncertain. Each thread has an incremental variable.VartotalcalculationsCapabilities. This may occur when a thread increments the variable but copies the valueVartotalasofnowPreviously, another thread may change its value by incrementing the variable. This will lead to the possibility that each thread is actually reporting incorrect results. Visual C # provides Statement to allow thread synchronization, so that each thread always returns accurate results.LockThe syntax 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.}
InputLockAfter the block, the execution of the specified expression is blocked until the specified thread has a special lock on the discussed object. In the example shown above, the execution of anobject is locked. You must useLock. Then, the execution continues in the form of blocks without being disturbed by other threads. The statement set executed as a unit is called "atom ". When} is encountered, the expression will be released and the thread can continue to work normally.
Add the lock statement to the application
- OpenCalculator. CS.
- Find each instance of the following code:
varTotalCalculations += 1;varTotalAsOfNow = varTotalCalculations;
There should be four instances of this Code, each of which has one.
- Modify the code to display it as follows:
lock(this){ varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations;}
- Save the job and test it as shown in the preceding example.
You may notice minor impact on program performance. This is because the thread execution stops when the component obtains the exclusive lock. Although it ensures correctness, this method offsets some performance advantages brought about by multithreading. The necessity of locking the thread should be carefully considered and implemented only when absolutely necessary.