방법: 스레드로부터 안전한 방식으로 컨트롤 호출(.NET Windows Forms)
다중 스레딩은 Windows Forms 앱의 성능을 향상시킬 수 있지만 Windows Forms 컨트롤에 대한 액세스는 기본적으로 스레드로부터 안전하지 않습니다. 다중 스레딩은 심각하고 복잡한 버그에 코드를 노출할 수 있습니다. 컨트롤을 조작하는 두 개 이상의 스레드가 강제로 컨트롤을 일관성 없는 상태로 만들고 경합 상태, 교착 상태, 중단 또는 중지로 이어질 수 있습니다. 앱에서 다중 스레딩을 구현할 때는 스레드로부터 안전한 방식으로 스레드 간 컨트롤을 호출해야 합니다. 자세한 내용은 관리형 스레딩 모범 사례를 참조하세요.
해당 컨트롤을 만들지 않은 스레드에서 Windows Forms 컨트롤을 안전하게 호출하는 방법에는 두 가지가 있습니다. System.Windows.Forms.Control.Invoke 메서드를 사용하여 주 스레드에서 만든 대리자를 호출하여 컨트롤을 호출합니다. 또는 System.ComponentModel.BackgroundWorker를 구현하고 이벤트 기반 모델을 사용하여 백그라운드 스레드에서 수행된 작업을 결과에 대한 보고와 분리합니다.
안전하지 않은 스레드 간 호출
컨트롤을 만들지 않은 스레드에서 직접 컨트롤을 호출하는 것은 안전하지 않습니다. 다음 코드 조각은 System.Windows.Forms.TextBox 컨트롤에 대한 안전하지 않은 호출을 보여 줍니다. Button1_Click
이벤트 처리기는 주 스레드의 WriteTextUnsafe
속성을 직접 설정하는 새로운 TextBox.Text 스레드를 만듭니다.
private void button1_Click(object sender, EventArgs e)
{
var thread2 = new System.Threading.Thread(WriteTextUnsafe);
thread2.Start();
}
private void WriteTextUnsafe() =>
textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe)
thread2.Start()
End Sub
Private Sub WriteTextUnsafe()
TextBox1.Text = "This text was set unsafely."
End Sub
Visual Studio 디버거는 스레드 간 작업이 잘못되었습니다. 컨트롤이 만든 스레드가 아닌 스레드에서 액세스되었습니다 메시지와 InvalidOperationException을 발생하여 이러한 안전하지 않은 스레드 호출을 검색합니다. InvalidOperationException은 Visual Studio 디버깅 중에 안전하지 않은 스레드 간 호출에 대해 항상 발생하며 앱 런타임에 발생할 수도 있습니다. 문제를 해결해야 하지만 Control.CheckForIllegalCrossThreadCalls 속성을 false
로 설정하여 예외를 사용하지 않도록 설정할 수 있습니다.
안전한 스레드 간 호출
다음 코드 예제에서는 컨트롤이 생성하지 않은 스레드에서 Windows Forms 컨트롤을 안전하게 호출하는 두 가지 방법을 보여 줍니다.
- System.Windows.Forms.Control.Invoke 메서드는 주 스레드에서 대리자를 호출하여 컨트롤을 호출합니다.
- System.ComponentModel.BackgroundWorker 구성 요소는 이벤트 기반 모델을 제공합니다.
두 예제에서 백그라운드 스레드는 해당 스레드에서 수행 중인 작업을 시뮬레이션하기 위해 1초 동안 일시 중단됩니다.
예제: Invoke 메서드 사용
다음 예제에서는 Windows Forms 컨트롤에 대한 스레드로부터 안전한 호출을 보장하는 패턴을 보여 줍니다. System.Windows.Forms.Control.InvokeRequired 속성을 쿼리하여 컨트롤이 생성한 스레드 ID를 호출하는 스레드 ID와 비교합니다. 다른 경우 Control.Invoke 메서드를 호출해야 합니다.
WriteTextSafe
를 통해 TextBox 컨트롤의 Text 속성을 새 값으로 설정할 수 있습니다. 메서드는 InvokeRequired를 쿼리합니다. InvokeRequired가 true
를 반환하는 경우 WriteTextSafe
는 재귀적으로 자신을 호출하여 이 메서드를 Invoke 메서드에 대한 대리자로 전달합니다. InvokeRequired가 false
를 반환하는 경우 WriteTextSafe
는 TextBox.Text를 직접 설정합니다. Button1_Click
이벤트 처리기는 새 스레드를 만들고 WriteTextSafe
메서드를 실행합니다.
private void button1_Click(object sender, EventArgs e)
{
var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
var thread2 = new System.Threading.Thread(threadParameters);
thread2.Start();
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
{
// Call this same method but append THREAD2 to the text
Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
textBox1.Invoke(safeWrite);
}
else
textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim threadParameters As New System.Threading.ThreadStart(Sub()
WriteTextSafe("This text was set safely.")
End Sub)
Dim thread2 As New System.Threading.Thread(threadParameters)
thread2.Start()
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (THREAD2)")
End Sub)
Else
TextBox1.Text = text
End If
End Sub
예: BackgroundWorker 사용
다중 스레딩을 구현하는 쉬운 방법은 이벤트 기반 모델을 사용하는 System.ComponentModel.BackgroundWorker 구성 요소를 사용하는 것입니다. 백그라운드 스레드는 주 스레드와 상호 작용하지 않는 BackgroundWorker.DoWork 이벤트를 발생시킵니다. 주 스레드는 주 스레드의 컨트롤을 호출할 수 있는 BackgroundWorker.ProgressChanged 및 BackgroundWorker.RunWorkerCompleted 이벤트 처리기를 실행합니다.
BackgroundWorker를 사용하여 스레드로부터 안전한 호출을 하려면 DoWork 이벤트를 처리합니다. 백그라운드 작업자가 상태를 보고하는 데 사용하는 이벤트에는 ProgressChanged 및 RunWorkerCompleted의 두 가지가 있습니다. ProgressChanged
이벤트는 상태 업데이트를 기본 스레드에 전달하는 데 사용되고 RunWorkerCompleted
이벤트는 백그라운드 작업자가 작업을 완료했다는 신호를 보내는 데 사용됩니다. 백그라운드 스레드를 시작하려면 BackgroundWorker.RunWorkerAsync를 호출합니다.
이 예제는 DoWork
이벤트에서 0에서 10까지 세며, 세는 사이에 1초 동안 잠시 멈춥니다. ProgressChanged 이벤트 처리기를 사용하여 기본 스레드에 다시 숫자를 보고하고 TextBox 컨트롤의 Text 속성을 설정합니다. ProgressChanged 이벤트를 사용하려면 BackgroundWorker.WorkerReportsProgress 속성을 true
로 설정해야 합니다.
private void button1_Click(object sender, EventArgs e)
{
if (!backgroundWorker1.IsBusy)
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int counter = 0;
int max = 10;
while (counter <= max)
{
backgroundWorker1.ReportProgress(0, counter.ToString());
System.Threading.Thread.Sleep(1000);
counter++;
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If (Not BackgroundWorker1.IsBusy) Then
BackgroundWorker1.RunWorkerAsync()
End If
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim counter = 0
Dim max = 10
While counter <= max
BackgroundWorker1.ReportProgress(0, counter.ToString())
System.Threading.Thread.Sleep(1000)
counter += 1
End While
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
TextBox1.Text = e.UserState
End Sub
.NET Desktop feedback