방법: 스레드로부터 안전한 방식으로 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)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}
Private Sub Button1_Click(ByVal sender As Object, e As EventArgs) Handles Button1.Click
    Thread2 = New Thread(New ThreadStart(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 컨트롤을 안전하게 호출하는 두 가지 방법을 보여 줍니다.

  1. System.Windows.Forms.Control.Invoke 메서드는 주 스레드에서 대리자를 호출하여 컨트롤을 호출합니다.
  2. System.ComponentModel.BackgroundWorker 구성 요소는 이벤트 기반 모델을 제공합니다.

두 예제에서 백그라운드 스레드는 해당 스레드에서 수행 중인 작업을 시뮬레이션하기 위해 1초 동안 일시 중단됩니다.

C# 또는 Visual Basic 명령줄에서 이러한 예제를 .NET Framework 앱으로 빌드하고 실행할 수 있습니다. 자세한 내용은 csc.exe를 사용한 명령줄 빌드 또는 명령줄에서 빌드(Visual Basic)를 참조하세요.

.NET Core 3.0부터 시작하여 .NET Core Windows Forms <폴더 이름>.csproj 프로젝트 파일이 있는 폴더에서 .NET Core Windows 앱으로 예제를 빌드하고 실행할 수도 있습니다.

예제: 대리자로 Invoke 메서드 사용

다음 예제에서는 Windows Forms 컨트롤에 대한 스레드로부터 안전한 호출을 보장하는 패턴을 보여 줍니다. System.Windows.Forms.Control.InvokeRequired 속성을 쿼리하여 컨트롤이 생성한 스레드 ID를 호출하는 스레드 ID와 비교합니다. 스레드 ID가 같으면 컨트롤을 직접 호출합니다. 스레드 ID가 다른 경우 주 스레드의 대리자를 사용하여 Control.Invoke 메서드를 호출하여 컨트롤에 대한 실제 호출을 만듭니다.

SafeCallDelegate를 통해 TextBox 컨트롤의 Text 속성을 설정할 수 있습니다. WriteTextSafe 메서드는 InvokeRequired를 쿼리합니다. InvokeRequiredtrue를 반환하는 경우 WriteTextSafeSafeCallDelegateInvoke 메서드에 전달하여 컨트롤에 대한 실제 호출을 만듭니다. InvokeRequiredfalse를 반환하는 경우 WriteTextSafeTextBox.Text를 직접 설정합니다. Button1_Click 이벤트 처리기는 새 스레드를 만들고 WriteTextSafe 메서드를 실행합니다.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }
}
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class InvokeThreadSafeForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New InvokeThreadSafeForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox
    Dim Thread2 as Thread = Nothing

    Delegate Sub SafeCallDelegate(text As String)

    Private Sub New()
        Button1 = New Button()
        With Button1
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
            .Text = "Set text safely"
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Thread2 = New Thread(New ThreadStart(AddressOf SetText))
        Thread2.Start()
        Thread.Sleep(1000)
    End Sub

    Private Sub WriteTextSafe(text As String)
        If TextBox1.InvokeRequired Then
            Dim d As New SafeCallDelegate(AddressOf SetText)
            TextBox1.Invoke(d, New Object() {text})
        Else
            TextBox1.Text = text
        End If
    End Sub

    Private Sub SetText()
        WriteTextSafe("This text was set safely.")
    End Sub
End Class

예: BackgroundWorker 이벤트 처리기 사용

다중 스레딩을 구현하는 쉬운 방법은 이벤트 기반 모델을 사용하는 System.ComponentModel.BackgroundWorker 구성 요소를 사용하는 것입니다. 백그라운드 스레드는 주 스레드와 상호 작용하지 않는 BackgroundWorker.DoWork 이벤트를 실행합니다. 주 스레드는 주 스레드의 컨트롤을 호출할 수 있는 BackgroundWorker.ProgressChangedBackgroundWorker.RunWorkerCompleted 이벤트 처리기를 실행합니다.

BackgroundWorker를 사용하여 스레드로부터 안전한 호출을 수행하려면 백그라운드 스레드에서 메서드를 만들어 작업을 수행하고 DoWork 이벤트에 바인딩합니다. 주 스레드에 다른 메서드를 만들어 백그라운드 작업의 결과를 보고하고 이를 ProgressChanged 또는 RunWorkerCompleted 이벤트에 바인딩합니다. 백그라운드 스레드를 시작하려면 BackgroundWorker.RunWorkerAsync를 호출합니다.

이 예제에서는 RunWorkerCompleted 이벤트 처리기를 사용하여 TextBox 컨트롤의 Text 속성을 설정합니다. ProgressChanged 이벤트를 사용하는 예제의 경우, BackgroundWorker를 참조하세요.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely with BackgroundWorker"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000);
        e.Result = "This text was set safely by BackgroundWorker.";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}
Imports System.ComponentModel
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class BackgroundWorkerForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New BackgroundWorkerForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents BackgroundWorker1 As BackgroundWorker
    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox

    Private Sub New()
        BackgroundWorker1 = New BackgroundWorker()
        Button1 = New Button()
        With Button1
            .Text = "Set text safely with BackgroundWorker"
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) _
     Handles BackgroundWorker1.DoWork
        ' Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000)
        e.Result = "This text was set safely by BackgroundWorker."
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) _
     Handles BackgroundWorker1.RunWorkerCompleted
        textBox1.Text = e.Result.ToString()
    End Sub
End Class

참고 항목