간단한 용어 정리
Invoke : 메인 쓰레드에서 동작하도록 해주는 기능 (다른 스레드 => 메인UI 스레드 동작 시켜줌)
InvokeRequired : Invoke가 필요한 상황인지 체크. (메인UI 스레드 != 현재 실행중인 스레드 체크)
Windows Form에서 UI는 메인 스레드에서만 변경이 가능하며 그 외의 접근에 대해서는 예외가 발생됩니다.
우선 사용 방법은 다음과 같습니다. 방법1 혹은 방법2를 참고하여 사용하면 됩니다.
(예외와 주의 사항은 더 아래쪽에 적어두겠습니다.)
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form3 : Form
{
//방법2를 위한 delegate ******************
private delegate void SetLabelDelegate();
private SetLabelDelegate setLabelDelegate;
//****************************************
private string message1 = "";
private string message2 = "";
private string message3 = "";
public Form3()
{
InitializeComponent();
//방법2를 위한 delegate 등록 *************
setLabelDelegate += SetLabel;
//****************************************
}
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ChangeUI);
message1 = "mainThread " + AppDomain.GetCurrentThreadId().ToString();
thread.Start();
}
private void ChangeUI ()
{
message2 += "subThread " + AppDomain.GetCurrentThreadId().ToString();
if (InvokeRequired)
{
//방법1. MethodInvoker, delegate 조합
Invoke((MethodInvoker)delegate
{
message3 = "InvokeThread " + AppDomain.GetCurrentThreadId().ToString();
SetLabel();
});
//방법2. delegate 호출
//Invoke(setLabelDelegate);
}
else
{
SetLabel();
}
}
private void SetLabel ()
{
label1.Text = message1;
label2.Text = message2;
label3.Text = message3;
}
}
}
결과 (SetLabel은 메인쓰레드 7724로 실행)
오류 예시.
[System.InvalidOperationException - Cross Thread]
: 메인 스레드가 아닌 다른 스레드에서 UI 변경을 한 경우 발생
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form3 : Form
{
public Form3()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(ChangeUI);
MessageBox.Show(AppDomain.GetCurrentThreadId().ToString()); //main thread number
thread.Start();
}
private void ChangeUI ()
{
MessageBox.Show(AppDomain.GetCurrentThreadId().ToString()); //other thread number
label1.Text = "1";
}
}
}
결과 : 당연하게 오류 발생
주의 사항)
쓰레드 번호를 확인할 수 있도록 코드를 작성해놨습니다.
이것으로 현재 함수가 어떤 쓰레드에서 실행 중인지 볼 수 있는데,
Invoke를 통해 MainThread -> SubThread -> MainThread 방식의 흐름이 가능합니다.
반드시 필요한 곳에서만 Invoke를 사용해야 올바른 비동기 코드가 진행이 될 수 있습니다.
잘 못된 코드와 올바른 코드 예시를 남깁니다.
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form3 : Form
{
public Form3()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread validInvokeThread = new Thread(ChangeUI);
validInvokeThread.Start();
//이렇게 호출되면 안됨.
//Thread invalidInvokeThread = new Thread(ChangeUI2);
//invalidInvokeThread.Start();
}
private void ChangeUI()
{
//스레드 동작과 메인UI 스레드가 원하는대로 잘 동작하게 됩니다.
SetLabel(label1, AppDomain.GetCurrentThreadId().ToString());
Thread.Sleep(1000);
SetLabel(label2, AppDomain.GetCurrentThreadId().ToString());
Thread.Sleep(1000);
SetLabel(label3, AppDomain.GetCurrentThreadId().ToString());
}
private void ChangeUI2()
{
//이렇게 사용하면 MainThread에서 모든 것을 처리해주게 됩니다.
//즉 "스레드의 사용 의미가 없다" 입니다.
if (InvokeRequired)
{
Invoke((MethodInvoker)delegate
{
ChangeUI2();
});
}
else
{
SetLabel(label1, AppDomain.GetCurrentThreadId().ToString());
Thread.Sleep(1000);
SetLabel(label2, AppDomain.GetCurrentThreadId().ToString());
Thread.Sleep(1000);
SetLabel(label3, AppDomain.GetCurrentThreadId().ToString());
}
}
private void SetLabel(Label label, string text) //delegate로 함수를 만들어서 처리도 가능
{
if (InvokeRequired)
{
Invoke((MethodInvoker)delegate
{
label4.Text = "메인스레드 : " + AppDomain.GetCurrentThreadId().ToString();
label.Text = text;
});
}
else
{
label4.Text = "메인스레드 : " + AppDomain.GetCurrentThreadId().ToString();
label.Text = text;
}
}
}
}
ChangeUI (UI는 메인에서, Sleep은 다른 스레드에서 처리)
|
ChangeUI2 (메인 스레드에서 처리. Sleep에 의해 멈추기도함)
|