연습: 백그라운드 작업을 사용하는 양식 구현

완료하는 데 시간이 오래 걸리는 작업이 있고 사용자 인터페이스(UI)에서 응답하지 않거나 차단하지 않도록 하려면 BackgroundWorker 클래스를 사용하여 다른 스레드에서 작업을 실행할 수 있습니다.

이 연습에서는 BackgroundWorker 클래스를 사용하여 사용자 인터페이스의 응답성을 유지하면서 시간이 많이 소요되는 계산을 “백그라운드”에서 실행하는 방법을 보여 줍니다. 완료하면 피보나치 수를 비동기적으로 계산하는 애플리케이션이 생깁니다. 큰 피보나치 수를 계산하는 데는 상당한 시간이 걸릴 수도 있지만 이 지연으로 주 UI 스레드가 중단되지 않으며 계산 중에도 폼이 응답하게 됩니다.

이 연습에서 설명하는 작업은 다음과 같습니다.

  • Windows 기반 애플리케이션 만들기

  • 양식에 BackgroundWorker 만들기

  • 비동기 이벤트 처리기 추가

  • 진행률 보고 및 취소에 대한 지원 추가

이 예제에 사용된 전체 코드 목록은 방법: 백그라운드 작업을 사용하는 폼 구현을 참조하세요.

백그라운드 작업을 사용하는 양식 만들기

  1. Visual Studio에서 BackgroundWorkerExample이라는 Wiindows 기반 애플리케이션 프로젝트를 만듭니다(파일>새로 만들기>프로젝트>Visual C# 또는 Visual Basic>클래식 데스크톱>Windows Forms 애플리케이션).

  2. 솔루션 탐색기에서 Form1을 마우스 오른쪽 단추로 클릭한 다음 바로 가기 메뉴에서 이름 바꾸기를 선택합니다. 파일 이름을 FibonacciCalculator로 변경합니다. 코드 요소 'Form1'에 대한 모든 참조 이름을 변경할지 묻는 메시지가 표시되면 단추를 클릭합니다.

  3. 도구 상자에서 NumericUpDown 컨트롤을 양식으로 끌어다 놓습니다. Minimum 속성을 1로 설정하고 Maximum 속성을 91로 설정합니다.

  4. 두 개의 Button 컨트롤을 양식에 추가합니다.

  5. 첫 번째 Button 컨트롤의 이름을 startAsyncButton로 바꾸고 Text 속성을 Start Async로 설정합니다. 두 번째 Button 컨트롤의 이름을 cancelAsyncButton로 바꾸고 Text 속성을 Cancel Async로 설정합니다. 해당 Enabled 속성을 false로 설정합니다.

  6. Button 컨트롤의 Click 이벤트 모두에 대한 이벤트 처리기를 만듭니다. 자세한 내용은 방법: 디자이너를 사용하여 이벤트 처리기 만들기를 참조하세요.

  7. 도구 상자에서 양식으로 Label 컨트롤을 끌어와서 이름을 resultLabel로 바꿉니다.

  8. 도구 상자에서 ProgressBar 컨트롤을 양식으로 끌어다 놓습니다.

디자이너로 BackgroundWorker 만들기

WindowsForms 디자이너를 사용하여 비동기 작업에 대한 BackgroundWorker를 만들 수 있습니다.

도구 상자구성 요소 탭에서 BackgroundWorker를 양식에 끌어다 놓습니다.

비동기 이벤트 처리기 추가

이제 BackgroundWorker 구성 요소의 비동기 이벤트에 대한 이벤트 처리기를 추가할 준비가 되었습니다. 백그라운드로 실행되는 시간이 많이 소요되는 작업(예: 피보나치 수 계산)은 이러한 이벤트 처리기 중 하나에 의해 호출됩니다.

  1. 속성 창에서 BackgroundWorker 구성 요소를 여전히 선택한 상태로 이벤트 버튼을 클릭합니다. DoWorkRunWorkerCompleted 이벤트를 두 번 클릭하여 이벤트 처리기를 만듭니다. 이벤트 처리기를 사용하는 방법에 대한 자세한 내용은 방법: 디자이너를 사용하여 이벤트 처리기 만들기를 참조하세요.

  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에 대한 첫 번째 매개 변수를 가져옵니다. BackgroundWorkerDoWorkEventArgs 매개 변수는 나중에 진행률 보고 및 취소 지원에 사용됩니다. 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 폼에서 변수 두 개를 선언합니다. 진행률을 추적하는 데 사용됩니다.

    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
    

검사점

이 시점에서 피보나치 계산기 애플리케이션을 컴파일하여 실행할 수 있습니다.

F5 키를 눌러 애플리케이션을 컴파일하고 실행합니다.

백그라운드로 계산이 실행되는 동안에는 ProgressBar에 완료될 때까지 계산 진행률이 표시됩니다. 보류 중인 작업도 취소할 수 있습니다.

작은 숫자에 대한 계산은 매우 빠르지만 큰 수에 대한 계산은 상당한 지연이 나타납니다. 값을 30 이상 입력하면 컴퓨터 속도에 따라 몇 초의 지연이 나타납니다. 값이 40을 넘으면 계산을 완료하는 데 몇 분 또는 몇 시간이 소요될 수 있습니다. 계산기가 큰 피보나치 수를 계산하는 동안 폼을 자유롭게 움직이고, 최소화 및 최대화하며, 해제할 수도 있습니다. 주 UI 스레드가 계산이 완료되기를 기다리지 않기 때문입니다.

다음 단계

이제 백그라운드에서 계산을 실행하기 위해 BackgroundWorker 구성 요소를 사용하는 양식을 구현했으며 비동기 작업에 대한 다른 가능성을 탐색할 수 있습니다.

참고 항목