チュートリアル: バックグラウンド操作を使用するフォームの実装

完了に時間がかかる操作がある場合、操作中にユーザー インターフェイス (UI) が応答を停止またはブロックしないようにするには、BackgroundWorker クラスを使用してその操作を別のスレッドで実行します。

このチュートリアルでは、BackgroundWorker クラスを使用して、時間のかかる計算を "バックグラウンドで" 実行し、ユーザー インターフェイスの応答性を維持する方法について説明します。 このチュートリアルを完了すると、フィボナッチ数を非同期に計算するアプリケーションが作成されます。 大きなフィボナッチ数の計算にはかなりの時間がかかることがありますが、この遅延によってメイン UI スレッドが中断されることはなく、計算中もフォームは応答性を維持します。

このチュートリアルでは、以下のタスクを行います。

  • Windows ベースのアプリケーションの作成

  • フォームの BackgroundWorker の作成

  • 非同期イベント ハンドラーの追加

  • 進行状況の報告とキャンセルのサポートの追加

この例で使用するコード全体については、「方法 : バックグラウンド操作を使用するフォームを実装する」を参照してください。

バックグラウンド操作を使用するフォームを作成する

  1. Visual Studio で、BackgroundWorkerExample という名前の Windows ベース アプリケーション プロジェクトを作成します ( [ファイル]>[新規]>[プロジェクト]>[Visual C#] または [Visual Basic]>[クラシック デスクトップ]>[Windows フォーム アプリケーション] )。

  2. ソリューション エクスプローラーで、[Form1] を右クリックし、ショートカット メニューの [名前の変更] をクリックします。 ファイル名を FibonacciCalculator に変更します。 コード要素 "Form1" へのすべての参照の名前を変更するかどうかをたずねられたら、[はい] をクリックします。

  3. NumericUpDown コントロールをツールボックスからフォームにドラッグします。 Minimum プロパティを 1 に、Maximum プロパティを 91 にそれぞれ設定します。

  4. 2 つの Button コントロールをフォームに追加します。

  5. 1 つ目の Button コントロールの名前を startAsyncButton に変更し、Text プロパティを Start Async に設定します。 2 つ目の Button コントロールの名前を cancelAsyncButton に変更し、Text プロパティを Cancel Async に設定します。 Enabled プロパティを false に設定します。

  6. Button コントロールの両方の Click イベントに対するイベント ハンドラーを作成します。 詳細については、「方法 : デザイナーを使用してイベント ハンドラーを作成する」を参照してください。

  7. Label コントロールをツールボックスからフォームにドラッグし、名前を resultLabel に変更します。

  8. ProgressBar コントロールをツールボックスからフォームにドラッグします。

デザイナーを使用して BackgroundWorker を作成する

非同期操作の BackgroundWorker は、Windowsフォーム デザイナーを使用して作成できます。

ツールボックス[コンポーネント] タブから、BackgroundWorker をフォームにドラッグします。

非同期イベント ハンドラーを追加する

これで、BackgroundWorker コンポーネントの非同期イベントに対するイベント ハンドラーを追加する準備ができました。 バックグラウンドで実行される時間のかかる操作 (フィボナッチ数の計算) は、これらのイベント ハンドラーのいずれかによって呼び出されます。

  1. [プロパティ] ウィンドウで、BackgroundWorker コンポーネントが選択されている状態で、 [イベント] ボタンをクリックします。 DoWork イベントと RunWorkerCompleted イベントをダブルクリックして、イベント ハンドラーを作成します。 イベント ハンドラーの使用方法の詳細については、「方法 : デザイナーを使用してイベント ハンドラーを作成する」を参照してください。

  2. フォームで 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.
    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
    
  3. DoWork イベント ハンドラーで、ComputeFibonacci メソッドへの呼び出しを追加します。 DoWorkEventArgsArgument プロパティから、ComputeFibonacci の最初のパラメーターを取得します。 BackgroundWorker パラメーターと DoWorkEventArgs パラメーターは、後で進行状況の報告とキャンセルのサポートに使用します。 ComputeFibonacci からの戻り値を、DoWorkEventArgsResult プロパティに割り当てます。 この結果は、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
    
  4. 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 
    
  5. 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 の呼び出しを検出し、ワーカー コード自体を中断できるようにするフラグもあります。

進行状況の報告を実装する

  1. [プロパティ] ウィンドウで backgroundWorker1 を選択します。 WorkerReportsProgressWorkerSupportsCancellation プロパティを true に設定します。

  2. FibonacciCalculator フォームで 2 つの変数を宣言します。 これらの変数を使用して進行状況を追跡します。

    int numberToCompute;
    int highestPercentageReached;
    
    private int numberToCompute = 0;
    private int highestPercentageReached = 0;
    
    Private numberToCompute As Integer = 0
    Private highestPercentageReached As Integer = 0
    
  3. 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
    

キャンセルのサポートを実装する

  1. 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
    
  2. 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

この時点で、フィボナッチ電卓アプリケーションをコンパイルして実行できます。

F5 キーを押してアプリケーションをコンパイルし、実行します。

計算がバックグラウンドで実行されている間、計算が完了するまでの進行状況が ProgressBar に表示されます。 また、保留中の操作をキャンセルすることもできます。

数値が小さい場合、計算に時間はかかりませんが、数値が大きくなると、大幅な遅延が発生します。 30 以上の値を入力した場合、コンピューターの速度によっては数秒の遅延が発生します。 値が 40 を超えると、計算が終了するまでに数分から数時間かかることがあります。 電卓が大きなフィボナッチ数を計算している間も、フォームの移動、最小化、最大化を自由に行うことができ、フォームを閉じることもできます。 これは、メイン UI スレッドが計算の終了を待機していないためです。

次のステップ

これで、BackgroundWorker コンポーネントを使用してバックグラウンドで計算を実行するフォームを実装できました。この後は、非同期操作の他の可能性を検討してください。

  • 複数の BackgroundWorker オブジェクトを使用して、複数の同時操作を実行する。

  • マルチスレッド アプリケーションをデバッグする方法については、「方法 : [スレッド] ウィンドウを使用する」を参照してください。

  • 非同期プログラミング モデルをサポートする独自のコンポーネントを実装する。 詳細については、「イベント ベースの非同期パターンの概要」を参照してください。

    注意事項

    どのような種類のマルチスレッドを使用している場合でも、非常に深刻で複雑なバグを引き起こしてしまう可能性があります。 マルチスレッドを使用するソリューションを実装する前に、「マネージド スレッド処理の実施」を参照してください。

関連項目