C# - 작업자(Worker) 스레드와 UI 스레드
작업자 스레드는 보통 UI 요소를 갖지 않는 스레드를 일컫습니다. 따라서, 다음과 같은 경우의 스레드는 보통 작업자 스레드라고 합니다.
Thread thread = new Thread(threadFunc);
thread.Start();
private void threadFunc()
{
    // 스레드 작업
}
이런 것도 작업자 스레드입니다.
ThreadPool.QueueUserWorkItem(
    (arg) =>
    {
        // 스레드 작업
    }, null);
반면 UI 스레드는 스레드에서 UI 요소를 생성해 사용하는 것을 말합니다. 그런데, UI 요소를 생성했다고 UI 스레드가 아닙니다. 그 UI 요소가 잘 동작하려면 메시지 펌프를 위한 메시지 루프 처리가 있어야 합니다. Win32에서는 다음과 같이 처리하던 코드입니다.
// https://learn.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{ 
    if (bRet == -1)
    {
        // handle the error and possibly exit
    }
    else
    {
        TranslateMessage(&msg); 
        DispatchMessage(&msg); 
    }
} 
// If threads are created without a message queue, why can I post to them immediately upon creation?
// ; https://devblogs.microsoft.com/oldnewthing/20241009-00/?p=110354
닷넷에서는 이러한 메시지 펌프를 Application.Run 함수에서 해줍니다. 따라서 다음의 스레드는 UI 스레드가 되는 것입니다.
Thread thread = new Thread(threadFunc);
thread.Start();
private void threadFunc()
{
    Application.Run(...[생략]...);
}
물론, 스레드 풀에서 빌려온 스레드일지라도 메시지 루프를 가지면 UI 스레드가 됩니다.
ThreadPool.QueueUserWorkItem(
    (arg) =>
    {
        Application.Run(...);
    }, null);
실제로 테스트를 해볼까요? ^^ Form2 윈도우 코드를 다음과 같이 만듭니다.
using System;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
    public partial class Form2 : Form
    {
        Timer _timer = new Timer(); // System.Windows.Forms.Timer는 타이머 처리를 
                                    // 내부적으로 윈도우 이벤트를 사용합니다.
        public Form2()
        {
            InitializeComponent();
            _timer.Interval = 1000;
            _timer.Tick += Timer_Tick;
            _timer.Start();
        }
        // 따라서, 아래의 함수는 이벤트 처리가 안되면 실행되지 않습니다.
        private void Timer_Tick(object sender, EventArgs e)
        {
            this.textBox1.Text = DateTime.Now.ToString();
        }
    }
}
위의 윈도우를 다음과 같이 작업자 스레드에서 실행해 봅니다.
ThreadPool.QueueUserWorkItem(
    (arg) =>
    {
        Form2 form = new Form2();
        form.Show();
    }, null);
그럼 윈도우까지는 뜨는데 텍스트 박스에 있는 시간이 업데이트가 안됩니다. 물론 그 외에도 WM_PAINT 같은 이벤트도 처리가 안되므로 화면이 마치 hang이 걸린 듯한 현상이 발생합니다.
반면, 다음과 같이 UI 스레드에서 실행하면,
ThreadPool.QueueUserWorkItem(
    (arg) =>
    {
        Form2 form = new Form2();
        Application.Run(form);
    }, null);
이번에는 타이머 처리가 잘 되어 텍스트 박스가 1초마다 내용이 업데이트됩니다.
자, 그럼 여기서 윈도우 프로젝트에 기본적으로 생성되는 Program.cs를 한번 열어봅니다.
static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}
바로 저 코드! 이제는 이해가 되시겠죠? 바로 메시지 루프이며 덕분에 UI 스레드로써 기능을 하고 있는 것입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]