逐步解說:實作使用背景作業的表單
如果您有作業需要很長一段時間才能完成,而且您不想要使用者介面 (UI) 停止回應或封鎖,您可以使用 BackgroundWorker 類別,在其他執行緒上執行作業。
此逐步解說示範如何使用 BackgroundWorker 類別以在「背景」執行耗費時間的計算,同時使用者介面仍然保持回應。 當您進行逐步解說時,您必須讓應用程式以非同步方式計算 Fibonacci 數字。 即使計算大量 Fibonacci 數字需要很長一段時間,主要 UI 執行緒不會受到此延遲的中斷,表單在計算期間仍然有回應。
這個逐步解說中所述的工作包括:
建立以 Windows 為基礎的應用程式
在您的表單中建立 BackgroundWorker
新增非同步事件處理常式
新增進度報告和支援取消作業
如需此範例中使用之程式碼的完整清單,請參閱如何:實作使用背景作業的表單。
建立使用背景作業的表單
在 Visual Studio 中,建立名為
BackgroundWorkerExample
的 Windows 應用程式專案 ([檔案]> [新增]> [專案]> [Visual C#] 或 [Visual Basic]> [傳統桌面]> [Windows Forms 應用程式])。在 [方案總管] 中,以滑鼠右鍵按一下 [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
您可以使用 BackgroundWorkerForms 設計工具建立用於非同步作業的 。
從 [工具箱] 的 [元件] 索引標籤,將 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
方法的呼叫。 從ComputeFibonacci
的 Argument 屬性取得 DoWorkEventArgs 的第一個參數。 BackgroundWorker 和 DoWorkEventArgs 參數稍後將用於進度報告和取消支援。 將ComputeFibonacci
的傳回值指派給 Result 的 DoWorkEventArgs 屬性。 此結果將可供 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 事件處理常式中,使用 ProgressBar 參數的 ProgressPercentage 屬性更新 ProgressChangedEventArgs。
// 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
檢查點
此時,您可以編譯並執行 Fibonacci 計算機應用程式。
按 F5 即可編譯和執行應用程式。
在背景執行計算時,您會看到 ProgressBar 顯示計算完成的進度。 您也可以取消暫止的作業。
對於小數字,計算應該非常快速,但是對於較大的數字,您應該會看到明顯的延遲。 如果您輸入的值為 30 或更大,您應該會看到幾秒鐘的延遲,取決於您的電腦速度。 對於大於 40 的值,可能需要數分鐘或數小時才能完成計算。 當計算機忙著計算大量 Fibonacci 數字時,請注意,您可以自由地到處移動表單、最小化、最大化,甚至是關閉表單。 這是因為主要 UI 執行緒並沒有在等待計算完成。
下一步
既然您已經實作表單,使用 BackgroundWorker 元件以在背景執行計算,您可以探索非同步作業的其他可能性:
針對數個同時作業使用多個 BackgroundWorker 物件。
若要針對多執行緒應用程式進行偵錯,請參閱如何:使用執行緒視窗。
實作您自己的元件,支援非同步程式設計模型。 如需詳細資訊,請參閱事件架構非同步模式概觀。
警告
無論使用何種多執行緒作業,您都可能會面臨嚴重而複雜的錯誤。 請在實作使用多執行緒的任何解決方案之前參閱 Managed 執行緒最佳做法。