演练:实现一个使用后台操作的窗体
如果某项操作需要很长的时间才能完成,并且不希望用户界面 (UI) 停止响应或“挂起”,则可以使用 BackgroundWorker 类在另一个线程中执行这种操作。
本演练演示如何使用 BackgroundWorker 类来在“后台”执行耗时的计算,同时用户界面保持响应。 演练时,将有一个异步计算斐波那契数列的应用程序。 即使计算很大的斐波那契数列需要花费大量的时间,但主 UI 线程不会被这种延时中断,并且在计算期间窗体仍会响应。
本演练涉及以下任务:
创建一个基于 Windows 的应用程序
在窗体中创建一个 BackgroundWorker
添加异步事件处理程序
添加进度报告和取消支持
要获得此示例中使用的代码的完整清单,请参见 如何:实现使用后台操作的窗体。
提示
显示的对话框和菜单命令可能会与“帮助”中的描述不同,具体取决于您现用的设置或版本。 若要更改设置,请在“工具”菜单上选择“导入和导出设置”。 有关更多信息,请参见 使用设置。
创建项目
第一步是创建项目并设置窗体。
创建使用后台操作的窗体
创建一个名为 BackgroundWorkerExample 的基于 Windows 的应用程序项目。 有关详细信息,请参见如何:创建新的 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 拖动到窗体上。
添加异步事件处理程序
现在已准备好为 BackgroundWorker 组件的异步事件添加事件处理程序。 这些事件处理程序将调用在后台运行的计算斐波那契数列的耗时操作。
实现异步事件处理程序
在**“属性”窗口中的 BackgroundWorker 组件仍处于选中状态时,单击“事件”**按钮。 双击 DoWork 和 RunWorkerCompleted 事件来创建事件处理程序。 有关如何使用事件处理程序的更多信息,请参见如何:使用设计器创建事件处理程序。
在窗体中创建一个称为 ComputeFibonacci 的新方法。 此方法完成实际的工作,并在后台运行。 这些代码演示了斐波那契数列算法的递归实现,这种算法的效率非常低,对于较大的数值花费的时间按指数增长。 在这里使用是出于演示的目的,为了说明在应用程序中某项操作可能带来长时间的延迟。
' 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
// 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. 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; }
在 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 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 'backgroundWorker1_DoWork
// 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, // 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 ); }
在 startAsyncButton 控件的 Click 事件处理程序中,添加启动异步操作的代码。
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
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); }
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 ); }
在 RunWorkerCompleted 事件处理程序中,将计算结果分配给 resultLabel 控件。
' 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 'backgroundWorker1_RunWorkerCompleted
// 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. 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; }
添加进度报告和取消支持
由于异步操作将会花费很长的时间,因此通常希望向用户报告进度并允许用户取消操作。 BackgroundWorker 类提供一种在后台操作进行时允许发送进度消息的事件。 它还提供一种允许辅助代码检测到 CancelAsync 调用并中断自身的标记。
实现进度报告
在**“属性”**窗口中,选择 backgroundWorker1。 将 WorkerReportsProgress 和 WorkerSupportsCancellation 属性设置为 true。
在 FibonacciCalculator 窗体中声明两个变量。 这将用来跟踪进度。
Private numberToCompute As Integer = 0 Private highestPercentageReached As Integer = 0
private int numberToCompute = 0; private int highestPercentageReached = 0;
int numberToCompute; int highestPercentageReached;
为 ProgressChanged 事件添加事件处理程序。 在 ProgressChanged 事件处理程序中,用 ProgressChangedEventArgs 参数的 ProgressPercentage 属性更新 ProgressBar。
' 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
// 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. void backgroundWorker1_ProgressChanged( Object^ /*sender*/, ProgressChangedEventArgs^ e ) { this->progressBar1->Value = e->ProgressPercentage; }
实现取消支持
在 cancelAsyncButton 控件的 Click 事件处理程序中,添加取消异步操作的代码。
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 'cancelAsyncButton_Click
private void cancelAsyncButton_Click(System.Object sender, System.EventArgs e) { // Cancel the asynchronous operation. this.backgroundWorker1.CancelAsync(); // Disable the Cancel button. cancelAsyncButton.Enabled = false; }
void cancelAsyncButton_Click( System::Object^ /*sender*/, System::EventArgs^ /*e*/ ) { // Cancel the asynchronous operation. this->backgroundWorker1->CancelAsync(); // Disable the Cancel button. cancelAsyncButton->Enabled = false; }
下面的 ComputeFibonacci 方法中的代码片段可报告进程并支持取消。
If worker.CancellationPending Then e.Cancel = True ... ' 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
if (worker.CancellationPending) { 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); }
if ( worker->CancellationPending ) { 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 ); }
检查点
此时,您就可以编译并运行斐波那契数列计算器应用程序了。
测试项目
按 F5 以编译并运行应用程序。
在后台运行计算的同时,将会看到 ProgressBar 显示完成计算的进度。 也可以取消未完成的操作。
对于很小的数值,计算应非常快,但对于较大的数值,将看到明显的延时。 如果输入 30 或更大的值,应看到有几秒钟的延时,这取决于计算机的速度。 对于大于 40 的值,完成计算可能要花费数分钟或数小时。 在计算器计算很大的斐波那契数列时,注意可以自由地移动窗体、最小化、最大化甚至关闭它。 这是因为主 UI 线程并不等待计算完成。
后续步骤
现在已经实现了一个利用 BackgroundWorker 组件来在后台执行计算的窗体,可以研究异步操作的其他可能性:
在同时进行的几项操作中使用多个 BackgroundWorker 对象。
要调试多线程应用程序,请参见 如何:使用“线程”窗口。
实现自己的支持异步编程模式的组件。 有关更多信息,请参见 基于事件的异步模式概述。
警告
使用任何一种多线程都可能引起极为严重和复杂的 bug。 在实现任何使用多线程的解决方案之前,请参考 托管线程处理的最佳做法。