演练:使用 Visual C# 创作简单的多线程组件

BackgroundWorker 组件取代了 System.Threading 命名空间并添加了功能;但是,可以选择保留 System.Threading 命名空间以实现向后兼容并供将来使用。 有关更多信息,请参见 BackgroundWorker 组件概述

您可以编写能同时执行多个任务的应用程序。 这种能力称为“多线程处理”或“自由线程处理”,是设计占用处理器资源并且要求用户进行输入的组件的一种有效方式。 计算工资表信息的组件就是一个可能利用多线程处理的组件示例。 该组件可以在一个线程上处理用户输入到数据库的数据,而在另一个线程上执行频繁使用处理器的工资表计算。 通过在不同的线程上运行这些进程,用户不必等到计算机完成计算,就可以输入其他数据。 在本演练中,将创建一个简单的多线程组件,该组件可以同时执行若干个复杂计算。

创建项目

您的应用程序由单个窗体和一个组件构成。 用户将输入值并指示该组件开始计算。 然后,窗体接收来自组件的值,并在标签控件中显示这些值。 组件执行需要大量使用处理器的计算,并在完成计算时通知窗体。 您将在组件中创建公共变量,用以保存从用户界面收到的值。 另外还要在组件中实现一些方法,根据这些变量的值执行计算。

提示

尽管函数对于计算值的方法更可取,但不能在线程之间传递参数,也不能返回值。 有很多向线程提供值和从线程接收值的简单方法。 在本演示中,您将通过更新公共变量将值返回到用户界面,当线程执行完毕后,使用事件通知主程序。

显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于您现用的设置或版本。 若要更改设置,请在“工具”菜单上选择“导入和导出设置”。 有关更多信息,请参见 使用设置

创建窗体

  1. 创建新的**“Windows 应用程序”**项目。

  2. 将应用程序命名为 Calculations,将 Form1.cs 重命名为 frmCalculations.cs。 当 Visual Studio 提示您重命名 Form1 代码元素时,请单击**“是”**。

    该窗体将用作应用程序的主用户界面。

  3. 向窗体中添加五个 Label 控件、四个 Button 控件和一个 TextBox 控件。

  4. 为这些控件设置属性,如下所示:

    控件

    名称

    文本

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

    InvokeBeginInvoke 需要将适当方法的委托作为参数。 这些代码行声明一些委托签名,这些签名将被 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. 对 FactorialMinusHandler 和 Fact1Handler、AddTwoHandler 和 Add2Handler 以及 LoopDoneHandler 和 LDoneHandler 重复上面的步骤。

    完成后,FactorialHandler、Factorial1Handler、AddTwoHandler 和 LoopDoneHandler 中应该没有剩余的代码,它们以前包含的所有代码都应该已经移到适当的新方法中。

  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. 保存工作,并按上例所示进行测试。

    您可能注意到对程序性能的细微影响。 这是因为当组件获得排他锁后,线程的执行停止。 尽管它保证了正确性,但这种方法抵消了多线程带来的某些性能优点。 应该认真考虑锁定线程的必要性,并且仅当绝对必要时才予以实现。

请参见

任务

如何:协调多个执行线程

演练:用 Visual Basic 创作简单的多线程组件

参考

BackgroundWorker

概念

基于事件的异步模式概述

其他资源

使用组件编程

组件编程演练

组件中的多线程处理