可以編寫能同時執行多個任務的應用程序。此能力(稱為“多線程處理”或“自由線程處理”)是設計處理器密集型且要求用戶輸入的組件的強大方法。計算工資表信息的組件就是一個可能利用多線程處理的組件示例。該組件可以在一個線程上處理用戶輸入到數據庫的數據,而在另一個線程上執行頻繁使用處理器的工資表計算。通過在不同的線程上運行這些進程,用戶不必等到計算機完成計算,就可以輸入其他數據。在本演練中,將創建一個簡單的多線程組件,該組件可以同時執行若干個復雜計算。
創建項目
應用程序將包括單個窗體和一個組件。用戶將輸入值並指示該組件開始計算。然後,窗體將接收來自該組件的值,將其顯示在標簽控件中。該組件將執行頻繁使用處理器的計算,並在完成後通知窗體。您將在組件中創建公共變量,用以保存從用戶界面收到的值。同時,您還將在組件中實現一些方法,根據這些變量的值執行計算。
注意 盡管對於計算值的方法來說,函數通常更為可取,但不能在線程之間傳遞參數,也不能返回值。有很多向線程提供值和從線程接收值的簡單方法。在本演示中,將通過更新公共變量將值返回到用戶界面,當線程執行完畢後,使用事件來通知主程序。
創建窗體
創建新的“Windows 應用程序”項目。
將應用程序命名為 Calculations,並將 Form1.cs 重命名為 frmCalculations.cs。
該窗體將用作應用程序的主用戶界面。
雙擊設計器上的窗體以打開代碼編輯器。在“編輯”菜單中,選擇“查找和替換”,然後選擇“替換”。使用“全部替換”將 Form1 替換為 frmCalculations。
在“解決方案資源管理器”中,右擊“frmCalculations.cs”並選擇“視圖設計器”。設計器打開。
向窗體中添加 5 個 Label 控件、4 個 Button 控件和 1 個 TextBox 控件。
為這些控件設置屬性,如下所示:
控件
名稱
文本
Label1
lblFactorial1
(空白)
Label2
lblFactorial2
(空白)
Label3
lblAddTwo
(空白)
Label4
lblRunLoops
(空白)
Label5
lblTotalCalculations
(空白)
Button1
btnFactorial1
Factorial
Button2
btnFactorial2
Factorial - 1
Button3
btnAddTwo
Add Two
Button4
btnRunLoops
Run a Loop
Textbox1
txtValue
(空白)
創建 Calculator 組件
從“項目”菜單中選擇“添加組件”。
將組件命名為 Calculator。
向 Calculator 組件添加公共變量
為 Calculator 打開代碼編輯器。
添加創建公共變量的語句,這些變量用於將值從 frmCalculations 傳遞給每個線程。
變量 varTotalCalculations 將保留該組件執行的計算總數的累計值,而其他變量將接收來自窗體的值。
public int varAddTwo;
public int varFact1;
public int varFact2;
public int varLoopValue;
public double varTotalCalculations = 0;
向 Calculator 組件添加方法和事件
為事件聲明委托,組件將使用這些事件向窗體傳遞值。
注意 盡管您將聲明 4 個事件,但由於其中的兩個事件將具有相同的簽名,因此只需要創建 3 個委托。
緊接著上一步輸入的變量聲明的下方,鍵入下列代碼:
// 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);
聲明組件將用來與應用程序進行通信的事件。為實現此目的,緊接著上一步輸入的代碼的下方,添加下列代碼。
public event FactorialCompleteHandler FactorialComplete;
public event FactorialCompleteHandler FactorialMinusOneComplete;
public event AddTwoCompleteHandler AddTwoComplete;
public event LoopCompleteHandler LoopComplete;
緊接著上一步鍵入的代碼的下方,鍵入下列代碼:
// 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.
varTotalCalculatio
[1] [2] [3] [4] [5] [6] 下一頁
ns += 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);
}
將用戶輸入傳輸到組件
下一步是向 frmCalculations 添加代碼,以接收用戶輸入,以及從 Calculator 組件接收值和向它傳輸值。
實現 frmCalculations 的前端功能
在代碼編輯器中打開 frmCalculations。
找到 public class frmCalculations 語句。緊接著 { 的下方鍵入:
Calculator Calculator1;
找到構造函數。緊接著 } 之前,添加以下行:
// Creates a new instance of Calculator.
Calculator1 = new Calculator();
在設計器中單擊每個按鈕,為每個控件的單擊事件處理程序生成代碼大綱,並添加代碼以創建這些處理程序。
完成後,單擊事件處理程序應該類似於以下形式:
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
上一頁 [1] [2] [3] [4] [5] [6] 下一頁
= "Looping";
Calculator1.RunALoop();
}
在上一步添加的代碼的下方,鍵入以下代碼以處理窗體將從 Calculator1 接收的事件:
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();
}
在 frmCalculations 的構造函數中,緊挨在 } 之前添加下列代碼,以處理窗體將從 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);
測試應用程序
現在項目已經創建完畢,該項目將能夠執行若干個復雜計算的組件與窗體結合在一起。盡管尚未實現多線程處理功能,但在繼續之前應該對項目進行測試,以驗證它的功能。
測試項目
從“調試”菜單中選擇“啟動”。
應用程序啟動並顯示 frmCalculations。
在文本框中鍵入 4,然後單擊標記為“添加兩個”的按鈕。
在按鈕下方的標簽中應該顯示數字“6”,在 lblTotalCalculations 中應該顯示“Total Calculations are 1”。
現在單擊標記為“階乘 - 1”的按鈕。
該按鈕的下方應顯示數字“6”,而 lblTotalCalculations 中現在應顯示“Total Calculations are 4”。
將文本框中的值更改為 20,然後單擊標記為“階乘”的按鈕。
該按鈕的下方顯示數字“2.43290200817664E+18”,而 lblTotalCalculations 中現在顯示為“Total Calculations are 24”。
將文本框中的值更改為 50000,然後單擊標記為“運行循環”的按鈕。
注意,在此按鈕重新啟用前有一個短暫然而明顯的間隔。此按鈕下的標簽應顯示“50000”,而總的計算次數顯示為“25000024”。
將文本框中的值更改為 5000000 並單擊標記為“運行循環”的按鈕,緊接著單擊標記為“添加兩個”的按鈕。再次單擊它。
直到循環已經完成,該按鈕以及窗體上的任何控件才有響應。
如果程序只運行單個執行線程,則類似上述示例的頻繁使用處理器的計算傾向於占用該程序,直到計算已經完成。在下一節中,您將向應用程序添加多線程處理功能,以便一次可以運行多個線程。
添加多線程處理功能
上面的示例演示了只運行單個執行線程的應用程序的限制。在下一節,您將使用 Thread 類對象向組件添加多個執行線程。
添加 Threads 子例程
在代碼編輯器中打開 Calculator.cs。
在代碼頂部附近,找到類聲明,緊接著 { 的下方,鍵入下列代碼:
// 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;
上一頁 [1] [2] [3] [4] [5] [6] 下一頁
>
在代碼底部緊接著類聲明結尾之前,添加以下方法:
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;
}
}
當實例化 Thread 對象時,它要求一個 ThreadStart 對象形式的參數。ThreadStart 對象是一個指向開始線程的方法的地址的委托。ThreadStart 對象不能接受參數或者傳遞值,因此只能表示 void 方法。剛才實現的 ChooseThreads 方法將從調用它的程序接收一個值,並使用該值來確定要啟動的適當線程。
向 frmCalculations 添加適當的代碼
在代碼編輯器中打開 frmCalculations.cs 文件,然後找到 protected void btnFactorial1_Click。
注釋掉直接調用 Calculator1.Factorial1 方法的行,如下所示:
// Calculator1.Factorial()
添加下列行,以調用 Calculator1.ChooseThreads 方法:
// Passes the value 1 to Calculator1, thus directing it to start the
// correct thread.
Calculator1.ChooseThreads(1);
對其他 button_click 子例程作類似的修改。
注意 一定要為 Threads 參數包含適當的值。
完成後,代碼看起來應該類似以下形式:
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);
}
封送處理對控件的調用
現在將加速窗體上的顯示更新。鑒於控件總是由主執行線程所有,從屬線程中對控件的任何調用都需要“封送處理”調用。封送處理是跨線程邊界移動調用的行為,需要耗費大量的資源。為了使需要發生的封送處理量減到最少,並確保以線程安全方式處理調用,應使用 Control.BeginInvoke 方法來調用主執行線程上的方法,從而使必須
上一頁 [1] [2] [3] [4] [5] [6] 下一頁
發生的跨線程邊界的封送處理量減到最少。當調用操作控件的方法時,這種調用非常必要。有關詳細信息,請參見從線程操作控件。
創建控件調用過程
為 frmCalculations 打開代碼編輯器。在聲明部分,添加下列代碼:
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 和 BeginInvoke 需要將適當方法的委托作為參數。這些代碼行聲明一些委托簽名,這些簽名將被 BeginInvoke 用於調用適當的方法。
在代碼中添加下列空方法。
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)
{
}
在“編輯”菜單中,使用“剪切”和“粘貼”,從 FactorialHandler 方法中剪切所有代碼,並將其粘貼到 FactHandler 中。
對 FactorialMinusHandler 和 Fact1Handler、AddTwoHandler 和 Add2Handler 以及 LoopDoneHandler 和 LDoneHandler 重復上面的步驟。
完成後,在 FactorialHandler、Factorial1Handler、AddTwoHandler 和 LoopDoneHandler 中應該沒有剩余代碼,並且它們曾經包含的所有代碼應該已經移動到適當的新方法中。
調用 BeginInvoke 方法以異步調用這些方法。可以從窗體 (this) 或者窗體上的任何控件調用 BeginInvoke。
完成後,代碼看起來應該類似以下形式:
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});
}
看起來似乎事件處理程序僅僅是對下一個方法進行調用。實際上,該事件處理程序實現了在主操作線程上調用方法。這種方法可節省跨線程邊界的調用,並使多線程應用程序能夠有效運行而不必擔心導致死鎖。有關在多線程環境下使用控件的詳細信息,請參見從線程操作控件。
保存您的工作。
從“調試”菜單中選擇“啟動”,測試該解決方案。
在文本框內鍵入 10000000 並單擊“運行循環”。
此按鈕下方的標簽中顯示“Looping”。運行這個循環應該占用很長時間。如果它完成得太快,請相應地調整該數字的大小。
連續地快速單擊仍在啟用的三個按鈕。您會發現所有按鈕都響應您的輸入。在“Add Two”下方的標簽應該第一個顯示結果。結果稍後將顯示在階乘按鈕下方的標簽中。估計這些結果會無限大,因為 10,000,000 的階乘返回的數字對於雙精度變量而言太大,以至超出了它包含的范圍。最後,再過片刻,結果將返回到“運行循環”按鈕下方。
正如剛剛觀察到的,在四個單獨的線程上同時執行四組獨立的計算。用戶界面保持對輸入的響應,並在每個線程完成後返回結果。
上一頁 [1] [2] [3] [4] [5] [6] 下一頁
協調線程
有經驗的多線程應用程序用戶可能會發現已鍵入的代碼中存在細微缺陷。從 Calculator.cs 中每個執行計算的子例程中撤回以下代碼行:
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
這兩行代碼遞增公共變量 varTotalCalculations 並將局部變量 varTotalAsOfNow 設為此值。然後,該值被返回給 frmCalculations,並顯示在標簽控件中。但返回的值正確嗎?如果只有單個執行線程在運行,則答案明顯是正確的。但是如果有多個線程在運行,答案則變得不太確定。每個線程都具有遞增變量 varTotalCalculations 的能力。有可能出現這樣的情況:在一個線程遞增該變量之後,但在它將該值復制到 varTotalAsOfNow 之前,另一個線程可能通過遞增該變量而更改它的值。這將導致有可能每個線程實際上在報告不正確的結果。Visual C# 提供 lock 語句語句以允許線程的同步,從而確保每個線程始終返回准確的結果。lock 的語法如下所示:
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.
}
輸入 lock 塊後,在指定的線程對所討論的對象擁有專用鎖之前,對指定表達式的執行一直被堵塞。在上面顯示的示例中,對 AnObject 的執行處於鎖定狀態。必須對返回引用的對象(而非返回值的對象)使用 lock。然後,執行以塊的形式繼續進行,而不會受到其他線程的干擾。作為一個單元執行的語句集稱為“原子”。當遇到 } 時,表達式將被釋放,線程可繼續正常工作。
將 lock 語句添加到應用程序
在代碼編輯器中打開 Calculator.cs。
找到下列代碼的每個實例:
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
應該有此代碼的四個實例,每個計算方法中有一個。
修改此代碼,使其顯示為如下形式:
lock(this)
{
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
}
保存工作,並按上例所示進行測試。
您可能注意到對程序性能的細微影響。這是因為當組件獲得排他鎖後,線程的執行停止。盡管它保證了正確性,但這種方法抵消了多線程帶來的某些性能優點。應該認真考慮鎖定線程的必要性,並且僅當絕對必要時才予以實現。
上一頁 [1] [2] [3] [4] [5] [6]