演练:实现使用后台操作的窗体
如果某项操作需要很长的时间才能完成,并且不希望用户界面 (UI) 停止响应或阻塞,则可以使用 BackgroundWorker 类在另一个线程上执行此操作。
本演练演示如何使用 BackgroundWorker 类“在后台”执行耗时的计算,同时用户界面保持响应。 演练时,将有一个异步计算 Fibonacci 数列的应用程序。 即使计算大型 Fibonacci 数列需要花费大量时间,但主 UI 线程不会被这种延时中断,并且在计算期间窗体仍会响应。
本演练涉及以下任务:
创建基于 Windows 的应用程序
在窗体中创建 BackgroundWorker
添加异步事件处理程序
添加进度报告和取消支持
若要了解此示例中使用的代码的完整列表,请参阅如何:实现使用后台操作的窗体。
创建使用后台操作的窗体
在 Visual Studio 中,创建一个名为
BackgroundWorkerExample
的基于 Windows 的应用程序项目(“文件”>“新建”>“项目”>“Visual C#”或“Visual Basic”>“经典桌面”>“Windows 窗体应用程序”)。在“解决方案资源管理器”中,右键单击“Form1”,然后从快捷菜单中选择“重命名”。 将文件名更改为
FibonacciCalculator
。 询问是否希望重命名对代码元素“”的所有引用时,单击“是”Form1
按钮。从“工具箱”将 NumericUpDown 控件拖到窗体上。 将 Minimum 属性设置为
1
,将 Maximum 属性设置为91
。向窗体添加两个 Button 控件。
重命名第一个 Button 控件
startAsyncButton
,并将 Text 属性设置为Start Async
。 重命名第二个 Button 控件cancelAsyncButton
,并将 Text 属性设置为Cancel Async
。 将它的 Enabled 属性设置为false
。为两个 Button 控件的 Click 事件创建一个事件处理程序。 有关详细信息,请参阅如何:使用设计器创建事件处理程序。
从“工具箱”将 Label 控件拖到窗体上,然后将其重命名为
resultLabel
。从“工具箱”将 ProgressBar 控件拖到窗体上。
使用设计器创建 BackgroundWorker
可以使用“Windows 窗体设计器”为异步操作创建 BackgroundWorker。
从“工具箱”的“组件”选项卡中,将 BackgroundWorker 拖到窗体上。
添加异步事件处理程序
现在已准备好为 BackgroundWorker 组件的异步事件添加事件处理程序。 这些事件处理程序将调用在后台运行的计算 Fibonacci 数列的耗时操作。
在“属性”窗口中的 BackgroundWorker 组件仍处于选中状态时,单击“事件”按钮。 双击 DoWork 和 RunWorkerCompleted 事件以创建事件处理程序。 若要深入了解如何使用事件处理程序,请参阅如何:使用设计器创建事件处理程序。
在窗体中新建一个名为
ComputeFibonacci
的新方法。 此方法完成实际的工作,并在后台运行。 这些代码演示了 Fibonacci 算法的递归实现,这种算法的效率非常低,对于较大的数值花费的时间按指数增长。 在这里使用是出于演示的目的,为了说明在应用程序中某项操作可能带来长时间的延迟。// This is the method that does the actual work. For this // example, it computes a Fibonacci number and // reports progress as it does its work. long ComputeFibonacci( int n, BackgroundWorker^ worker, DoWorkEventArgs ^ e ) { // The parameter n must be >= 0 and <= 91. // Fib(n), with n > 91, overflows a long. if ( (n < 0) || (n > 91) ) { throw gcnew ArgumentException( "value must be >= 0 and <= 91","n" ); } long result = 0; // Abort the operation if the user has cancelled. // Note that a call to CancelAsync may have set // CancellationPending to true just after the // last invocation of this method exits, so this // code will not have the opportunity to set the // DoWorkEventArgs.Cancel flag to true. This means // that RunWorkerCompletedEventArgs.Cancelled will // not be set to true in your RunWorkerCompleted // event handler. This is a race condition. if ( worker->CancellationPending ) { e->Cancel = true; } else { if ( n < 2 ) { result = 1; } else { result = ComputeFibonacci( n - 1, worker, e ) + ComputeFibonacci( n - 2, worker, e ); } // Report progress as a percentage of the total task. int percentComplete = (int)((float)n / (float)numberToCompute * 100); if ( percentComplete > highestPercentageReached ) { highestPercentageReached = percentComplete; worker->ReportProgress( percentComplete ); } } return result; }
// This is the method that does the actual work. For this // example, it computes a Fibonacci number and // reports progress as it does its work. long ComputeFibonacci(int n, BackgroundWorker worker, DoWorkEventArgs e) { // The parameter n must be >= 0 and <= 91. // Fib(n), with n > 91, overflows a long. if ((n < 0) || (n > 91)) { throw new ArgumentException( "value must be >= 0 and <= 91", "n"); } long result = 0; // Abort the operation if the user has canceled. // Note that a call to CancelAsync may have set // CancellationPending to true just after the // last invocation of this method exits, so this // code will not have the opportunity to set the // DoWorkEventArgs.Cancel flag to true. This means // that RunWorkerCompletedEventArgs.Cancelled will // not be set to true in your RunWorkerCompleted // event handler. This is a race condition. if (worker.CancellationPending) { e.Cancel = true; } else { if (n < 2) { result = 1; } else { result = ComputeFibonacci(n - 1, worker, e) + ComputeFibonacci(n - 2, worker, e); } // Report progress as a percentage of the total task. int percentComplete = (int)((float)n / (float)numberToCompute * 100); if (percentComplete > highestPercentageReached) { highestPercentageReached = percentComplete; worker.ReportProgress(percentComplete); } } return result; }
' This is the method that does the actual work. For this ' example, it computes a Fibonacci number and ' reports progress as it does its work. Function ComputeFibonacci( _ ByVal n As Integer, _ ByVal worker As BackgroundWorker, _ ByVal e As DoWorkEventArgs) As Long ' The parameter n must be >= 0 and <= 91. ' Fib(n), with n > 91, overflows a long. If n < 0 OrElse n > 91 Then Throw New ArgumentException( _ "value must be >= 0 and <= 91", "n") End If Dim result As Long = 0 ' Abort the operation if the user has canceled. ' Note that a call to CancelAsync may have set ' CancellationPending to true just after the ' last invocation of this method exits, so this ' code will not have the opportunity to set the ' DoWorkEventArgs.Cancel flag to true. This means ' that RunWorkerCompletedEventArgs.Cancelled will ' not be set to true in your RunWorkerCompleted ' event handler. This is a race condition. If worker.CancellationPending Then e.Cancel = True Else If n < 2 Then result = 1 Else result = ComputeFibonacci(n - 1, worker, e) + _ ComputeFibonacci(n - 2, worker, e) End If ' Report progress as a percentage of the total task. Dim percentComplete As Integer = _ CSng(n) / CSng(numberToCompute) * 100 If percentComplete > highestPercentageReached Then highestPercentageReached = percentComplete worker.ReportProgress(percentComplete) End If End If Return result End Function
在 DoWork 事件处理程序中,添加对
ComputeFibonacci
方法的调用。 从 DoWorkEventArgs 的 Argument 属性中获取ComputeFibonacci
的第一个参数。 稍后将 BackgroundWorker 和 DoWorkEventArgs 参数用于进度报告和取消支持。 将ComputeFibonacci
的返回值分配给 DoWorkEventArgs 的 Result 属性。 此结果将可供 RunWorkerCompleted 事件处理程序使用。注意
DoWork 事件处理程序不直接引用
backgroundWorker1
实例变量,因为这将会使此事件处理程序和某个特定的 BackgroundWorker 实例耦合。 相反,引发此事件的 BackgroundWorker 引用将从sender
参数恢复。 当窗体承载多个 BackgroundWorker 时这非常重要。 在 DoWork 事件处理程序中不操作任何用户界面对象也非常重要。 而应该通过 BackgroundWorker 事件与用户界面进行通信。// This event handler is where the actual, // potentially time-consuming work is done. void backgroundWorker1_DoWork( Object^ sender, DoWorkEventArgs^ e ) { // Get the BackgroundWorker that raised this event. BackgroundWorker^ worker = dynamic_cast<BackgroundWorker^>(sender); // Assign the result of the computation // to the Result property of the DoWorkEventArgs // object. This is will be available to the // RunWorkerCompleted eventhandler. e->Result = ComputeFibonacci( safe_cast<Int32>(e->Argument), worker, e ); }
// This event handler is where the actual, // potentially time-consuming work is done. private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { // Get the BackgroundWorker that raised this event. BackgroundWorker worker = sender as BackgroundWorker; // Assign the result of the computation // to the Result property of the DoWorkEventArgs // object. This is will be available to the // RunWorkerCompleted eventhandler. e.Result = ComputeFibonacci((int)e.Argument, worker, e); }
' This event handler is where the actual work is done. Private Sub backgroundWorker1_DoWork( _ ByVal sender As Object, _ ByVal e As DoWorkEventArgs) _ Handles backgroundWorker1.DoWork ' Get the BackgroundWorker object that raised this event. Dim worker As BackgroundWorker = _ CType(sender, BackgroundWorker) ' Assign the result of the computation ' to the Result property of the DoWorkEventArgs ' object. This is will be available to the ' RunWorkerCompleted eventhandler. e.Result = ComputeFibonacci(e.Argument, worker, e) End Sub
在
startAsyncButton
控件的 Click 事件处理程序中,添加启动异步操作的代码。void startAsyncButton_Click( System::Object^ /*sender*/, System::EventArgs^ /*e*/ ) { // Reset the text in the result label. resultLabel->Text = String::Empty; // Disable the UpDown control until // the asynchronous operation is done. this->numericUpDown1->Enabled = false; // Disable the Start button until // the asynchronous operation is done. this->startAsyncButton->Enabled = false; // Enable the Cancel button while // the asynchronous operation runs. this->cancelAsyncButton->Enabled = true; // Get the value from the UpDown control. numberToCompute = (int)numericUpDown1->Value; // Reset the variable for percentage tracking. highestPercentageReached = 0; // Start the asynchronous operation. backgroundWorker1->RunWorkerAsync( numberToCompute ); }
private void startAsyncButton_Click(System.Object sender, System.EventArgs e) { // Reset the text in the result label. resultLabel.Text = String.Empty; // Disable the UpDown control until // the asynchronous operation is done. this.numericUpDown1.Enabled = false; // Disable the Start button until // the asynchronous operation is done. this.startAsyncButton.Enabled = false; // Enable the Cancel button while // the asynchronous operation runs. this.cancelAsyncButton.Enabled = true; // Get the value from the UpDown control. numberToCompute = (int)numericUpDown1.Value; // Reset the variable for percentage tracking. highestPercentageReached = 0; // Start the asynchronous operation. backgroundWorker1.RunWorkerAsync(numberToCompute); }
Private Sub startAsyncButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles startAsyncButton.Click ' Reset the text in the result label. resultLabel.Text = [String].Empty ' Disable the UpDown control until ' the asynchronous operation is done. Me.numericUpDown1.Enabled = False ' Disable the Start button until ' the asynchronous operation is done. Me.startAsyncButton.Enabled = False ' Enable the Cancel button while ' the asynchronous operation runs. Me.cancelAsyncButton.Enabled = True ' Get the value from the UpDown control. numberToCompute = CInt(numericUpDown1.Value) ' Reset the variable for percentage tracking. highestPercentageReached = 0 ' Start the asynchronous operation. backgroundWorker1.RunWorkerAsync(numberToCompute) End Sub
在 RunWorkerCompleted 事件处理程序中,将计算结果分配给
resultLabel
控件。// This event handler deals with the results of the // background operation. void backgroundWorker1_RunWorkerCompleted( Object^ /*sender*/, RunWorkerCompletedEventArgs^ e ) { // First, handle the case where an exception was thrown. if ( e->Error != nullptr ) { MessageBox::Show( e->Error->Message ); } else if ( e->Cancelled ) { // Next, handle the case where the user cancelled // the operation. // Note that due to a race condition in // the DoWork event handler, the Cancelled // flag may not have been set, even though // CancelAsync was called. resultLabel->Text = "Cancelled"; } else { // Finally, handle the case where the operation // succeeded. resultLabel->Text = e->Result->ToString(); } // Enable the UpDown control. this->numericUpDown1->Enabled = true; // Enable the Start button. startAsyncButton->Enabled = true; // Disable the Cancel button. cancelAsyncButton->Enabled = false; }
// This event handler deals with the results of the // background operation. private void backgroundWorker1_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e) { // First, handle the case where an exception was thrown. if (e.Error != null) { MessageBox.Show(e.Error.Message); } else if (e.Cancelled) { // Next, handle the case where the user canceled // the operation. // Note that due to a race condition in // the DoWork event handler, the Cancelled // flag may not have been set, even though // CancelAsync was called. resultLabel.Text = "Canceled"; } else { // Finally, handle the case where the operation // succeeded. resultLabel.Text = e.Result.ToString(); } // Enable the UpDown control. this.numericUpDown1.Enabled = true; // Enable the Start button. startAsyncButton.Enabled = true; // Disable the Cancel button. cancelAsyncButton.Enabled = false; }
' This event handler deals with the results of the ' background operation. Private Sub backgroundWorker1_RunWorkerCompleted( _ ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) _ Handles backgroundWorker1.RunWorkerCompleted ' First, handle the case where an exception was thrown. If (e.Error IsNot Nothing) Then MessageBox.Show(e.Error.Message) ElseIf e.Cancelled Then ' Next, handle the case where the user canceled the ' operation. ' Note that due to a race condition in ' the DoWork event handler, the Cancelled ' flag may not have been set, even though ' CancelAsync was called. resultLabel.Text = "Canceled" Else ' Finally, handle the case where the operation succeeded. resultLabel.Text = e.Result.ToString() End If ' Enable the UpDown control. Me.numericUpDown1.Enabled = True ' Enable the Start button. startAsyncButton.Enabled = True ' Disable the Cancel button. cancelAsyncButton.Enabled = False End Sub
添加进度报告和取消支持
由于异步操作将会花费很长的时间,因此通常希望向用户报告进度并允许用户取消操作。 BackgroundWorker 类提供一个在后台操作进行时允许发送进度消息的事件。 它还提供允许辅助代码检测对 CancelAsync 的调用并中断自身的标记。
实现进度报告
在“属性”窗口中,选择
backgroundWorker1
。 将 WorkerReportsProgress 和 WorkerSupportsCancellation 属性设置为true
。在
FibonacciCalculator
窗体中声明两个变量。 这将用于跟踪进度。int numberToCompute; int highestPercentageReached;
private int numberToCompute = 0; private int highestPercentageReached = 0;
Private numberToCompute As Integer = 0 Private highestPercentageReached As Integer = 0
为 ProgressChanged 事件添加事件处理程序。 在 ProgressChanged 事件处理程序中,使用 ProgressChangedEventArgs 参数的 ProgressPercentage 属性更新 ProgressBar。
// This event handler updates the progress bar. void backgroundWorker1_ProgressChanged( Object^ /*sender*/, ProgressChangedEventArgs^ e ) { this->progressBar1->Value = e->ProgressPercentage; }
// This event handler updates the progress bar. private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar1.Value = e.ProgressPercentage; }
' This event handler updates the progress bar. Private Sub backgroundWorker1_ProgressChanged( _ ByVal sender As Object, ByVal e As ProgressChangedEventArgs) _ Handles backgroundWorker1.ProgressChanged Me.progressBar1.Value = e.ProgressPercentage End Sub
实现取消支持
在
cancelAsyncButton
控件的 Click 事件处理程序中,添加取消异步操作的代码。void cancelAsyncButton_Click( System::Object^ /*sender*/, System::EventArgs^ /*e*/ ) { // Cancel the asynchronous operation. this->backgroundWorker1->CancelAsync(); // Disable the Cancel button. cancelAsyncButton->Enabled = false; }
private void cancelAsyncButton_Click(System.Object sender, System.EventArgs e) { // Cancel the asynchronous operation. this.backgroundWorker1.CancelAsync(); // Disable the Cancel button. cancelAsyncButton.Enabled = false; }
Private Sub cancelAsyncButton_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cancelAsyncButton.Click ' Cancel the asynchronous operation. Me.backgroundWorker1.CancelAsync() ' Disable the Cancel button. cancelAsyncButton.Enabled = False End Sub
下面的
ComputeFibonacci
方法中的代码片段可报告进程并支持取消。if ( worker->CancellationPending ) { e->Cancel = true; }
if (worker.CancellationPending) { e.Cancel = true; }
If worker.CancellationPending Then e.Cancel = True
// Report progress as a percentage of the total task. int percentComplete = (int)((float)n / (float)numberToCompute * 100); if ( percentComplete > highestPercentageReached ) { highestPercentageReached = percentComplete; worker->ReportProgress( percentComplete ); }
// Report progress as a percentage of the total task. int percentComplete = (int)((float)n / (float)numberToCompute * 100); if (percentComplete > highestPercentageReached) { highestPercentageReached = percentComplete; worker.ReportProgress(percentComplete); }
' Report progress as a percentage of the total task. Dim percentComplete As Integer = _ CSng(n) / CSng(numberToCompute) * 100 If percentComplete > highestPercentageReached Then highestPercentageReached = percentComplete worker.ReportProgress(percentComplete) End If
Checkpoint
此时,可以编译并运行 Fibonacci 计算器应用程序。
按 F5 编译并运行应用程序。
在后台运行计算的同时,将会看到 ProgressBar 显示完成计算的进度。 也可以取消挂起的操作。
对于较小数值,计算应非常快,但对于较大数值,将看到明显的延时。 如果输入 30 或更大的值,应看到有几秒钟的延时,这取决于计算机的速度。 对于大于 40 的值,完成计算可能要花费数分钟或数小时。 在计算器计算较大的 Fibonacci 数列时,注意可以自由地移动窗体、最小化、最大化甚至关闭窗体。 这是因为主 UI 线程不会等待计算完成。
后续步骤
现在已经实现了一个使用 BackgroundWorker 组件来在后台执行计算的窗体,可以探究异步操作的其他可能性:
在同时进行的几项操作中使用多个 BackgroundWorker 对象。
若要调试多线程应用程序,请参阅如何:使用“线程”窗口。
实现自己的支持异步编程模式的组件。 有关详细信息,请参阅基于事件的异步模式概述。
注意
使用任何一种多线程都可能引起极为严重和复杂的 Bug。 在实现任何使用多线程处理的解决方案之前,请参阅托管线程处理最佳做法。