演練:使用VS2010 C# 創做簡單的多線程組件

BackgroundWorker 組件取代了 System.Threading 命名空間並添加了功能;可是,能夠選擇保留 System.Threading 命名空間以實現向後兼容並供未來使用。有關更多信息,請參見 BackgroundWorker 組件概述前端

您能夠編寫能同時執行多個任務的應用程序。 這種能力稱爲「多線程處理」或「自由線程處理」,是設計佔用處理器資源而且要求用戶進行輸入的組件的一種有效方式。 計算工資表信息的組件就是一個可能利用多線程處理的組件示例。 該組件能夠在一個線程上處理用戶輸入到數據庫的數據,而在另外一個線程上執行頻繁使用處理器的工資表計算。 經過在不一樣的線程上運行這些進程,用戶沒必要等到計算機完成計算,就能夠輸入其餘數據。 在本演練中,將建立一個簡單的多線程組件,該組件能夠同時執行若干個複雜計算。數據庫

建立項目


您的應用程序由單個窗體和一個組件構成。 用戶將輸入值並指示該組件開始計算。 而後,窗體接收來自組件的值,並在標籤控件中顯示這些值。 組件執行須要大量使用處理器的計算,並在完成計算時通知窗體。 您將在組件中建立公共變量,用以保存從用戶界面收到的值。 另外還要在組件中實現一些方法,根據這些變量的值執行計算。express

注意

儘管函數對於計算值的方法更可取,但不能在線程之間傳遞參數,也不能返回值。 有不少向線程提供值和從線程接收值的簡單方法。 在本演示中,您將經過更新公共變量將值返回到用戶界面,當線程執行完畢後,使用事件通知主程序。windows

顯示的對話框和菜單命令可能會與「幫助」中的描述不一樣,具體取決於您現用的設置或版本。 若要更改設置,請在「工具」菜單上選擇「導入和導出設置」 有關更多信息,請參見 使用設置api

建立窗體

  1. 建立新的「Windows 應用程序」項目。安全

  2. 將應用程序命名爲 Calculations,將 Form1.cs 重命名爲 frmCalculations.cs 當 Visual Studio 提示您重命名 Form1 代碼元素時,請單擊「是」多線程

    該窗體將用做應用程序的主用戶界面。app

  3. 向窗體中添加五個 Label 控件、四個 Button 控件和一個 TextBox 控件。異步

  4. 爲這些控件設置屬性,以下所示:async

     

    控件

    名稱

    文本

    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 組件

  1. 「項目」菜單中選擇「添加組件」

  2. 將組件命名爲 Calculator

向 Calculator 組件添加公共變量

  1. 打開 Calculator「代碼編輯器」

  2. 添加建立公共變量的語句,這些變量用於將值從 frmCalculations 傳遞給每一個線程。

    變量 varTotalCalculations 將保留該組件執行的計算總次數的累計值,而其餘變量將接收來自窗體的值。

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

向 Calculator 組件添加方法和事件

  1. 爲事件聲明委託,組件將使用這些事件向窗體傳遞值。

    注意

    儘管您將聲明 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);
    
  2. 聲明組件將用來與應用程序進行通訊的事件。 爲實現此目的,緊接着上一步輸入的代碼的下方,添加下列代碼。

    public event FactorialCompleteHandler FactorialComplete;
    public event FactorialCompleteHandler FactorialMinusOneComplete;
    public event AddTwoCompleteHandler AddTwoComplete;
    public event LoopCompleteHandler LoopComplete;
    
  3. 緊接着上一步鍵入的代碼的下方,鍵入下列代碼:

    // 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);
    }
    

將用戶輸入傳輸到組件


下一步是向 frmCalculations 中添加代碼,以接收用戶輸入以及從 Calculator 組件接收值和向它傳送值。

實現 frmCalculations 的前端功能

  1. 「代碼編輯器」中打開 frmCalculations

  2. 找到 public partial class frmCalculations 語句。 在緊接着 { 的下方鍵入:

    Calculator Calculator1;
    
  3. 找到構造函數。 在緊挨着 } 的前面,添加下面的代碼行:

    // Creates a new instance of Calculator.
    Calculator1 = new Calculator();
    
  4. 在設計器中單擊每一個按鈕,爲每一個控件的 Click 事件處理程序生成代碼大綱,並添加建立這些處理程序的代碼。

    完成後,Click 事件處理程序應以下所示:

    // 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. 在上一步添加的代碼的後面,鍵入如下代碼以處理窗體將從 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. 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);
    

測試應用程序


如今項目已建立完畢,它將可以執行若干複雜計算的組件與窗體結合在一塊兒。 儘管還沒有實現多線程處理功能,但在繼續以前應該對項目進行測試,以驗證它的功能。

測試項目

  1. 「調試」菜單中選擇「啓動調試」

    應用程序開始運行,出現 frmCalculations

  2. 在文本框中鍵入 4,而後單擊標記爲「Add Two」的按鈕。

    按鈕下方的標籤中應該顯示數字「6」,lblTotalCalculations 中應該顯示「Total Calculations are 1」。

  3. 如今單擊標記爲「Factorial - 1」的按鈕。

    該按鈕的下方應顯示數字「6」,而 lblTotalCalculations 中如今應顯示「Total Calculations are 4」。

  4. 將文本框中的值更改成 20,而後單擊標記爲「Factorial」的按鈕。

    該按鈕的下方顯示數字「2.43290200817664E+18」,而 lblTotalCalculations 中如今顯示爲「Total Calculations are 24」。

  5. 將文本框中的值更改成 50000,而後單擊標記爲「Run A Loop」的按鈕。

    注意,在該按鈕從新啓用以前存在一個很短但可察覺到的間隔。 此按鈕下的標籤應顯示「50000」,而總的計算次數顯示爲「25000024」。

  6. 將文本框中的值更改成 5000000 並單擊標記爲「Run A Loop」的按鈕,緊接着單擊標記爲「Add Two」的按鈕。 再次單擊它。

    直到循環已經完成,該按鈕以及窗體上的任何控件纔有響應。

    若是程序只運行單個執行線程,則相似上述示例的頻繁使用處理器的計算傾向於佔用該程序,直到計算已經完成。 在下一節中,您將嚮應用程序添加多線程處理功能,以便一次能夠運行多個線程。

添加多線程處理功能


上面的示例演示了只運行單個執行線程的應用程序的限制。 在下一節中,您將使用 Thread 類向組件添加多個執行線程。

添加 Threads 子例程

  1. 「代碼編輯器」中打開「Calculator.cs」

  2. 在代碼頂部附近,找到類聲明,緊接着 { 的下方,鍵入如下代碼:

    // 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. 在代碼底部緊接着類聲明結尾以前,添加如下方法:

    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 添加適當的代碼

  1. 「代碼編輯器」中打開「frmCalculations.cs」文件,而後找到 private void btnFactorial1_Click

    1. 註釋掉直接調用 Calculator1.Factorial1 方法的行,以下所示:

      // Calculator1.Factorial()
      
    2. 添加下面的行,以調用 Calculator1.ChooseThreads 方法:

      // Passes the value 1 to Calculator1, thus directing it to start the 
      // correct thread.
      Calculator1.ChooseThreads(1);
      
  2. 對其餘 button_click 方法進行相似的修改。

    注意

    請務必包括 Threads 參數的適當值。

    完成後,代碼看起來應該相似下面這樣:

    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);
    }
    

封送對控件的調用

 

如今您將加速窗體的顯示更新。 因爲控件老是由主執行線程全部,所以從從屬線程中調用任何控件都須要「封送處理」調用。 封送就是跨越線程邊界移動調用的行爲,須要消耗大量的資源。 爲了使須要發生的封送處理量減到最少,並確保以線程安全的方式處理調用,應使用 Control.BeginInvoke 方法來調用主執行線程上的方法,從而使必須發生的跨線程邊界的封送處理量減到最少。 當調用操做控件的方法時,必須使用這種調用。 有關詳細信息,請參見 如何:從線程中操做控件

建立控件調用過程

  1. 打開 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 用來調用適當的方法。

  2. 在代碼中添加下列空方法。

    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. 「編輯」菜單中,使用「剪切」「粘貼」,從 FactorialHandler 方法中剪切全部代碼,並將其粘貼到 FactHandler 中。

  4. FactorialMinusHandlerFact1HandlerAddTwoHandlerAdd2Handler 以及 LoopDoneHandlerLDoneHandler 重複上面的步驟。

    完成後,FactorialHandlerFactorial1HandlerAddTwoHandlerLoopDoneHandler 中應該沒有剩餘的代碼,它們之前包含的全部代碼都應該已經移到適當的新方法中。

  5. 調用 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});
    }
    

    看起來彷佛事件處理程序僅僅是對下一個方法進行調用。 實際上,該事件處理程序實現了在主操做線程上調用方法。 這種方法可節省跨線程邊界的調用,並使多線程應用程序可以有效運行而沒必要擔憂致使死鎖。 有關在多線程環境中使用控件的詳細信息,請參見 如何:從線程中操做控件

  6. 保存您的工做。

  7. 經過從「調試」菜單中選擇「啓動調試」來測試您的解決方案。

    1. 在文本框內鍵入 10000000 並單擊「Run A Loop」

      此按鈕下方的標籤中顯示「Looping」。 運行這個循環應該佔用很長時間。 若是它完成得太快,請相應地調整該數字的大小。

    2. 連續地快速單擊仍在啓用的三個按鈕。 您會發現全部按鈕都響應您的輸入。 「Add Two」下方的標籤應該第一個顯示結果。 結果稍後將顯示在階乘按鈕下方的標籤中。 估計這些結果會無限大,由於 10,000,000 的階乘返回的數字對於雙精度變量而言太大,以致超出了它包含的範圍。 最後,再過片刻,結果將返回到「Run A Loop」按鈕的下方。

      正如剛剛觀察到的,在四個單獨的線程上同時執行四組獨立的計算。 用戶界面保持對輸入的響應,並在每一個線程完成後返回結果。

協調線程


有經驗的多線程應用程序用戶可能會發現已鍵入的代碼中存在細微缺陷。 Calculator 中每一個執行計算的方法撤回如下代碼行:

varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;

這兩行代碼遞增公共變量 varTotalCalculations 並將局部變量 varTotalAsOfNow 設置爲該值。 而後,該值返回給 frmCalculations,並顯示在標籤控件中。 可是否正在返回正確的值? 若是隻有單個執行線程在運行,則答案明顯是正確的。 可是若是有多個線程在運行,答案則變得不太肯定。 每一個線程都具備增長變量 varTotalCalculations 的能力。 有可能出現這樣的狀況:在一個線程增長該變量以後和將該值複製到 varTotalAsOfNow 以前的間隔裏,另外一個線程可能經過增長該變量而更改它的值。 這將致使有可能每一個線程實際上在報告不正確的結果。 Visual C# 提供 「鎖定」語句(C# 參考)來實現線程同步,從而確保每一個線程始終返回準確的結果。 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 語句添加到應用程序

  1. 「代碼編輯器」中打開「Calculator.cs」

  2. 找到下列代碼的每一個實例:

    varTotalCalculations += 1;
    varTotalAsOfNow = varTotalCalculations;
    

    應該有此代碼的四個實例,每一個計算方法中有一個。

  3. 修改此代碼,使其顯示爲以下形式:

    lock(this)
    {
       varTotalCalculations += 1;
       varTotalAsOfNow = varTotalCalculations;
    }
    
  4. 保存工做,並按上例所示進行測試。

    您可能注意到對程序性能的細微影響。 這是由於當組件得到排他鎖後,線程的執行中止。 儘管它保證了正確性,但這種方法抵消了多線程帶來的某些性能優勢。 應該認真考慮鎖定線程的必要性,而且僅當絕對必要時才予以實現。

相關文章
相關標籤/搜索